学免杀首先要了解代码特性,将各种trick(webshell相关)作用都发挥到极致,从而绕过所有waf,当然仅限于静态分析,动态的话使用aes加密即可。
本文将围绕php是个解释型语言的前提进行探索
代码执行优先级
代码执行优先级简单来说就是执行代码的顺序
经过测试php是从上到下,从左到右进行解释执行
优先执行括号内的内容,但是不会因为括号越多越先执行
1 |
|
因此这个例子的执行结果是phpinfo()
而这个例子是exit()
1 |
|
错误等级
php有四个常见Error,分别是Warning Error、Notice Error、Parse Error、Fatal Error
warning error
warning error不会阻止脚本运行,只会警告问题
常出现原因:
- 调用目录中不存在的外部文件
- 函数中的错误函数
1 | <?php |
可以看到第一行成功执行,第二行报warning error错误
Notice Error
Notice Error的等级比warning error的等级小,系统不确定是实际错误还是常规代码,常见于访问未定义的变量。
1 |
|
Parse Error
parse error会在解析语法出错时直接退出程序,已执行的代码不受影响。
常见于
- eval函数缺失
;
闭合
观察以下两个执行结果的差异
根据结果可知,在eval函数中优先检查php语句是否使用;
闭合,若未闭合则全部不予执行。
Fatal Error
Fatal Error常见于重复定义,会立即退出程序
常见于
- a—-表示重复定义的函数名;
- b—-第一次定义该函数时的文件名称及行号;
- c—-第二次定义该函数时的文件名称;
- d—-第二次定义该函数时的行号。
error_reporting函数
常常在webshell中看到error_reporting函数,主要作用是设置报告错误的等级。用于Webshell时可以屏蔽无关的错误报告,降低被防火墙或者IPS设备拦截的风险。
1 | //不报告NOTICE和WARNING错误 |
其他常见的错误常量见下
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执行结果
eval
eval — 把字符串作为PHP代码执行
1 | eval(string $code): mixed |
assert和eval的区别
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');");PHP5,不支持($a)()这种调用方式,但是PHP7中支持,如
('phpinfo')()
1
('assert')('system("whoami")');
- 在测试过程中发现assert的位置只能使用字符串,函数、define、引用均失效,意味着只能拆分成字符串拼接的形式。参数的位置不受限制
回调函数
回调函数即使用某一个参数作为函数,其他参数作为其参数的函数,最经典的就是call_user_func
函数
常见的有
1 | call_user_func(最经典) |
查找可用回调函数
目前常见的回调函数都(call_user_func等)被webshell查杀软件列入可疑名单,但是仍然有部分回调函数没有被找到,可通过以下方法快速查找。
去PHP官网查阅函数手册,查找可以用作后门的PHP回调函数,根据实际经验,利用下面五个关键词,能提高查找到拥有后门潜质的PHP回调函数的效率:
关键词一:callable
关键词二:mixed $options
关键词三:handler
关键词四:callback
关键词五:invoke
找到以下函数
1 | array_uintersect_assoc/array_uintersect |
array_uintersect_assoc — 带索引检查计算数组的交集,用回调函数比较数据
用法
1 | array_uintersect_assoc(array $array, array ...$arrays, callable $value_compare_func): array |
根据定义写一个一句话木马
1 |
|
同理还有
1 | array_intersect_uassoc |
其他函数
更多代码执行函数见以下文章介绍
字符串变形
在webshell免杀过程中,对关键字字符串进行处理能绕过静态分析,但是对污点分析和行为分析的检测引擎效果就不太理想。
常见字符串变形函数
1 | ucwords() //函数把字符串中每个单词的首字符转换为大写。 |
substr
substr — 返回字符串的子串
1 | substr(string $string, int $offset, ?int $length = null): string |
用法
1 | //返回从第3个索引位置开始的一个字符 |
strtr
转换字符串中特定的字符
1 | strtr(string,from,to) |
用法
1 |
|
trim
移除字符串两侧的空白字符或其他预定义字符。
1 | trim(string,charlist) |
用法
1 |
|
substr_replace
把字符串 string 的一部分替换为另一个字符串 replacement。
1 | substr_replace(string,replacement,start,length) |
用法
1 |
|
base64编码
base64已经烂大街了,WAF等安全设备也会自动解码,因此只作为基础编码跟其他的结合使用。
1 | echo base64_encode('assert'); |
异或
1 |
|
首先要知道的是两个字符串异或之后得到的还是一个字符串,所以我们可以用一些非字母数字的字符异或后变成我们想要的字符,但是说实话徒手找的话实在费劲,下面php代码生成列表
1 |
|
得到下面列表
1 | %81^%FF=>~ %82^%FF=>} %83^%FF=>| |
然后用这些字符构成函数,比如
1 | %9E^%FF=>a |
uuencode
1 | convert_uuencode("assert"); |
利用信息差绕过
当遇到动态沙箱检测时,会将脚本放置在沙箱中,记录脚本运行过程产生的行为特征。如果遇到污点跟踪,那么每个变量的值都会被跟踪记录,看起来无懈可击。
但是实际上我们可以利用信息差,比如注意到阿里云webshell检测会将文件以md5格式重命名,这样就不是我们实战上传的文件名。
同样的还有webdir+
文件名
那么我们就可以把assert的某些字符放入文件名,这里放一个LandGrey师傅的脚本
1 |
|
脚本名必须是”***s.php”的名字形式,即最后一位字符要为”s”,然后用”sclass” 和 hex2bin(“12101f040107”)的值按位异或,得到”assert”,从而利用回调函数,执行PHP代码。
上传到WEBDIR+系统后,脚本被重命名,”试执行时自然无法复现木马行为“,从而绕过了检测。这种方式有一种明显的要求,就是我们能够准确预知或控制脚本名的最后一位字符。
其他信息差
在针对某个特别的目标测试时,可以利用目标的特殊信息构造信息的差异,实现Webshell绕过。
如目标IP地址的唯一性、域名、特殊Cookie、Session字段和值、$_SERVER变量中可被控制的值,甚至是主机Web服务的根目录、操作系统等一些差别,发挥空间很大。
写文件函数
有时可以寄希望于将webshell编码,再通过写文件函数将内容写到webshell,然后再还原回来,因此写文件函数也至关重要
常见写文件函数
1 | file_put_contents(filename, data) |
利用ZipArchive类读写文件
任意文件写入
利用ZipArchive类写入文件的思路:将写入的内容先写进压缩包中,再进行解压缩至任意目录
1 |
|
利用方式(get请求):?tmpzip=/tmp/test.zip&filename=tgao.php&content=%3C%3Fphp%20phpinfo()%3B&filePath=/tmp
各个参数解释:确保tmpzip
参数和filePath
参数所表示的目录有写入权限
- tmpzip:临时创建的zip文件路径,需要将文件内容写入到zip文件
- filename:zip压缩包中的文件名,也是解压之后的文件名
- content:filename文件的内容
- filePath:压缩包解压到的路径
XMLWriter类
1 |
|
利用方式:?file=/tmp/test&content=hello
,也可以直接调用该类的方法
1 |
|
利用iconv_mime_decode编码fopen+fread+filesize函数
1 |
|
利用方式:?file=/etc/passwd
iconv_mime_encode可以将字符串编码为mime格式
1 |
|
利用iconv_mime_decode_headers编码file函数
1 |
|
利用方式:?file=/etc/passwd