内容纲要

前言

本章内容实际上应该放在Java RMI 原理/流程之前 . Java反序列化可以说是整个 Java 安全的起点 .

在此之前 , 我讨论过 PHP反序列化漏洞 , Python Pickle/CPickle反序列化漏洞 , Python PyYAML反序列化漏洞 . 现在 , 终于到了 Java反序列化漏洞 . 算是把主流的 Web 安全反序列化问题都整合到了一起 .

全文参考了很多前辈们的文章和观点 , 在此先感谢各位前辈们的帮助 !

注 : 由于我自己也不会 Java , 所以这部分内容面向基本没有Java基础的师傅 , 尽力把每个点都说清楚 .


Java Reflection

要想理解 Java 反序列化漏洞 , 就必须理解 Java 反射机制 . 因此本章会重点说明什么是 Java 反射机制 .


什么是 Java Reflection

可以用一句话来说明什么是 Java 反射机制

Java 反射机制允许运行中的Java程序获取自身的信息, 操作类和对象的内部属性.

当然, 这样说太简明了, 很多没了解过 Java 的同学( 比如我 )可能会一头雾水 . 往细一点说 , 可以给出如下定义 :

Java 反射机制是指在程序运行时 , 对于任何一个类 , 都能知道这个类的所有属性和方法 , 对于任何一个实例对象 , 都能调用该对象的任何一个属性和方法 .

Java中这种 " 动态获取信息 " 和 " 动态调用属性方法 " 的机制被称为 Java 反射机制.

实例对象可以通过反射机制获取它的类 , 类可以通过反射机制获取它的所有方法和属性 . 获取的属性可以设值 , 获取的方法可以调用 .

在静态语言中 , 一般对象的类型都是在编译期就确定下来的 . 而通过 Java 反射机制 , 可以动态的创建对象并调用其方法和属性 .

上面一行我提到了 " 动态 " 和 " 静态语言 " 这两个关键词 . 那么到底什么是 " 动态特性 " ? 什么是 " 静态语言呢 " ? 这点是必须要了解的 .


静态语言与的动态特性 .

  1. 什么是静态语言 ?

    先来说一下什么是动态语言 . 动态语言是指 : 可以在程序运行时改变程序结构和变量类型的语言 .

    具体是什么意思呢 ? 比如在程序运行时 , 新的类和对象可以被加载和创建 , 新的函数或方法可以被加入或者去除等等 .

    比如 Smalltalk , Ruby , Python , PHP , Lua , Perl , Groovy 等 , 都是动态语言. 反之则是静态语言 , 比如 C/C++ , Java , C# 等 .


  2. 什么是动态特性 ?

    动态语言具有的某些特性即为动态特性 . 下面以 PHP 为例 .

    Phith0n 师傅在 KCon2019 上详细说明了 PHP动态特性的捕捉与逃逸 这个议题 , PPT写的非常精彩 , 推荐精读一下.

    该议题中指出 : PHP语言的动态特性体现在 :

    一段 PHP 代码 , 其中的变量值的改变可能会导致这段代码产生功能上的变化 .

    举个例子 , 我们都知道 PHP 是一门弱类型语言 , 我们在编写代码时不需要指定变量的类型 , 在程序运行时解析器会自动检测变量的类型 , 还可以在运行时人为的改变变量的类型.

    事实上 , 大部分动动态语言都被设计成弱类型 , 只有当变量被赋值后 , 才能确定该变量的类型 , 当代码被执行时 , 才会去检测变量是否合法 .

    再举一个例子 , PHP中可以定义变量函数 .

    官方文档是这么定义 PHP变量函数( 可变函数 ) 的

    研究过 PHP 安全的师傅肯定对这个特性非常熟悉 , 因为很多WebShell都利用了该特性 , 比如 :

    在代码中是无法确定变量 $foo , $_ 含义的 , 只有当程序运行后变量被赋值时 , 才能确定其具体的功能和含义 . 这也是 PHP 的一种动态特性 , 可用于实现回调函数 .

    在 PHP中文网 PHP中动态特性学习笔记 一文中详细说明了 PHP 14个动态特性 , 有兴趣的师傅可以记录一下.


Java Reflection 功能

说了这么多 , 这个动态特性和 Java 反射有什么关系呢 ?

通过上面的例子不难发现 , 正是因为 PHP 中存在多种动态特性 , 使得开发人员能通过很少的代码来实现非常多的功能 . 比较典型的例子就是一句话木马 , 通过一行 <?php @eval($_POST[cmd]); 能实现目录管理 , 命令执行 , 数据库连接 , 文件上传下载等多种多样的功能 .

但是 Java 本身是一门静态语言 , 无法像 PHP 那么灵活多变 . 但是通过 Java 反射机制 , 可以为自身提供一些动态特性 . 当我们在通过 IDE 写代码时 , 敲击点号" . " , 会出现当前对象或类所包含的属性和方法 . 这里用到的就是 Java 反射机制 .

而且 , 反射最重要的用途是开发各种通用框架 . 很多框架都是通过XML文件来进行配置的( 例如 struts.xml , spring-*.xml 等 ) , 即所谓的框架核心配置文件 . 为了确保框架的通用性 , 程序运行时需要根据配置文件中对应的内容加载不同的类或对象 , 调用不同的方法 , 这也依赖于 Java 反射机制 .

综上所述 , Java 反射机制的功能可分为如下几点 :

  1. 在程序运行时查找一个对象所属的类 .
  2. 在程序运行时查找任意一个类的成员变量和方法 .
  3. 在程序运行时构造任意一个类的对象 .
  4. 在程序运行时调用任意一个对象的方法 .

下文将会逐一说明如何通过 Java 反射机制实现上述这些功能 .


查找一个对象所属的类

如何获取一个类( java.lang.Class )呢? 总的而言有三种方法 .

  • obj.getClass()
  • Class.forName(className)
  • className.class

具体的使用方法如下所示

针对不同的情况 , 可以用不同的方法来获取类 .

Phith0n师傅 代码审计知识星球的 <<Java安全漫谈 - 反射篇>> 里重点强调了 forName 这个方法 , 我觉得还是很有意思的, 这里记录一下 .

需要注意的是 : forName() 函数有两个重载 , 如下所示 :

Class.forName( String className )

Class.forName( String className , Boolean initialize , ClassLoader loader )

  1. String className : 类名
  2. Boolean initialize : 是否进行类初始化
  3. ClassLoader loader : 加载器( 告诉 Java 虚拟机如何加载获取的类 , Java 默认根据类名( 即类的绝对路径 , 例如 java.lang.Runtime() )来加载类 )

其中 , 第一种方法是对第二种方法的封装 , 存在以下对应关系 :
Class.forName( String className ) == Class.forName( String className , True , currentLoader )

那么这个类初始化是指什么呢 ? 我们来看下面这个 Demo .

结果表明 , 在 Java 类初始化时 , 会执行静态代码块中的内容 .

那也就是说 , 如果我们能控制一个类 , 那么就可以通过在类中添加包含恶意代码的静态代码块 . 当类初始化时 , 默认会自动执行恶意代码. 如下所示 :

  1. 假设存在如下代码 :

    此时 , 如果我们能控制 test2类 , 那么就能执行任意代码 .

  2. 构造恶意的 test2

    在该类静态代码块中 , 通过 java.lang.Runtime.getRuntime().exec() 执行系统命令 , 并将返回字节流转换为字符流 , 存入缓冲区后逐行读取并输出 .

  3. 当调用 vul 类时 , 会自动执行恶意代码 .

那么 test2.java 中的代码是如何的编写呢? 这又涉及到 Java 反射机制了 , 接着往下看 , 你就会明白 .


查找一个类的方法

如何获取某一个类的所有方法呢? 总的来说有三种方法 .

  • className.getMethod(functionName , [parameterTypes.class])
  • className.getMethods()
  • className.getDeclaredMethods()

具体使用方法如下图所示 , 这里参考了 sczyh30 师傅的 深入解析Java反射(1) , 在原有代码的基础上进行了修改 .

getMethod() : 返回类中一个特定的方法 . 其中第一个参数为方法名称 , 后面的参数为方法的参数对应 Class 的对象 .

getMethods() : 返回某个类的所有公用(public)方法 , 包括其继承类的公用方法 .

getDeclaredMethods() : 返回某个类或接口声明的所有方法 , 包括公共、保护、默认(包)访问和私有方法 , 但不包括其继承类的方法 .

补充一些内容 , 面向没有Java基础的师傅

  1. Class<?> : 定义了一个泛型类 , 其中 <?> 代表不确定类的类型 , 具体细节可以参考 程序鱼师傅 JAVA泛型通配符T,E,K,V区别,T以及Class,Class<?> 的区别 一文
  2. for(Method m:methods) 循环获取methods集合中的内容 , 把每一项赋值给变量m .
  3. 输出信息中的美元符号( $ )代表内部类 .

构造任意一个类的对象

上文提到了可以通过三种方式来获取类 , 那么如果获取一个实例对象呢 ?

通过 className.newInstance() 来构建一个实例对象.

我们都知道在类实例化时会调用构造函数 , 而构造函数又分为 " 有参构造函数 " 和 " 无参构造函数 " . 然而 className.newInstance() 没有参数 , 只能调用无参构造函数 . 如果我们想要调用有参构造函数 , 就必须依赖于 Class 类的 getConstructor() 方法 .

通过 Class 类的 getConstructor() 方法 , 可以获取 Constructor 类的一个实例 , Constructor 类也存在一个 newInstance() 方法 , 不过该方法可以携带参数 . 用该方法来创建实例对象可以调用有参构造函数 .

举个例子

  • className.newInstance()
  • className.getConstructor( parameterType ).newInstance( parameterName )

因此 , 我们可以通过 newInstance() 方法来构造任何一个类的对象 . 并且可以选择是调用其无参构造方法 , 还是有参的构造方法 .

如果想要了解 className.newInstance()className.getConstructor().newInstance() 两种反射方式的不同点 , 可以参考 程式前沿 <<通過Class.newInstance()和Constructor.newInstance()兩種反射方法建立物件的異同>> , 说的还是比较清楚的.


调用任意一个实例对象的方法

有了实例对象 , 如何调用调用该对象的方法呢 ?

一般来说 , 可以通过 objectName.functionName() 这种格式来调用实例方法 . 举个例子 .

但是在很多情况下 , 你并不知道类名, 也就无法 new 出实例对象 , 更别提调用实例对象的方法了 . 当遇到这种情况时 , 就需要使用 Java 反射来调用实例对象的方法了 .

仔细看过上文的师傅应该有一些思路了 .

  • 不知道类怎么办 ?
    我们可以通过 obj.getClass() , Class.forName(className) , className.class 来获取类**

  • 不知道类有哪些方法怎么办 ?
    我们可以通过 className.getMethod(functionName , [parameterTypes.class]) , className.getMethods() , className.getDeclaredMethods() 来获取类的方法.

  • 不能 new 出实例对象怎么办 ?
    我们可以通过 className.newInstance() , className.getConstructor().newInstance() 来构造实例对象 .

  • 那如何调用实例对象的方法呢 ?

    通过 invoke() 方法来调用任何一个实例对象的方法 !

来看下 invoke() 函数的定义

根据官方文档的定义 , 可以修改之前的代码 , 通过反射来调用实例对象的方法 .

  • Method.invoke(obj , args[])

如上文所说的 , 通过Java反射机制来获取类 , 获取类的方法 , 构造实力对象 , 最终调用实例方法 .

注 : 官方文档中提到了一些比较有意思的东西 , 需要注意 .

如果要调用的方法是静态的 , 则忽略 obj 参数 .
这个点其实比较好理解 ,
我们知道Java中调用静态方法是无需创建实例对象的** , 所以这里可以省略 obj 参数 .

如果要调用的方法的形参个数为 " 0 " , 那么 args[] 数组的长度可以为 " 0 " 或者 " null " .
这个点其实也没啥说的 , args[] 数组本就是要调用方法的参数 , 既然目标方法没有参数 , 这里自然也就不用写 .


Java 反射的内容到这基本就说完了 . 不过我还想结合 Phith0n师傅 文章再补充一些内容 . 毕竟我们最终的目的不是学习如何去编写代码 , 而是将 Java反射与反序列化结合在一起 , 更通俗的说 , 就是知道如何通过 Java 反射来执行命令 , 也就是所谓的 RCE( Remote Code Execute ) .

补充内容

java.lang.Runtime

很多师傅一看到这个类就来劲了 . 没错 , java.lang.Runtime 类的 exec() 方法是Java中最常见的执行命令的方式 ,

在没有了解 Java 反射机制前 , 我并不明白这行代码的含义 , 只会生搬硬套的使用 . 现在 , 来看一看它的原理到底是什么 .

  1. 正常情况下 , 我们想要拿到一个除系统类以外的类 , 必须要先 import 后才能使用 . 否则会出现 cannot find symbol 等报错 . 但是在进行利用时 , 系统是不会让你随意加载类的 .

    但是 , 通过 Class.forName(className) 获取类则没有这个限制 , 我们可以通过 forName() 方法加载任何类 .

    举个例子 :

  2. 拿到了 java.lang.Runtime 类后 , 我们肯定想知道该类可调用的方法 . 于是通过 className.getMethods() , className.getDeclaredMethods() 来获取类的方法

    从输出信息中可以找到我们想要执行的 exec() 方法

  3. 拿到了类 , 拿到了类的方法 , 就可以通过反射实例化对象并通过 invoke() 调用方法了

    发现程序抛出了异常 , 报错信息为 : Exception in thread "main" java.lang.IllegalAccessException: Class test7 can not access a member of class java.lang.Runtime with modifiers "private"

    简单的翻译一下 , 结果为 : test7 类无法访问 java.lang.Runtime 类中带有 "private" 修饰符的成员变量 / 成员函数 .

    这里就很有意思了 , 在第二步中输出类的方法时 , exec()方法的修饰符全部为 "public" . 那么这个 "private" 修饰符是哪来的呢 ?

    结合上文思考一下 , 您就能很快反应过来 :

    在通过 cls.newInstance() 构造实例对象时 , 会默认调用无参构造函数 . 难道这个无参构造函数是私有的吗 ?

    网上有很多 Java源码 的项目 , 很容易就能找到 java.lang.Runtime 类的源代码 .

    可以发现 , java.lang.Runtime 类的构造方法的确使用了 "private" 修饰符 . 我们知道 "private" 修饰符修饰的方法只能在当前类中被调用 , 外部是不可见的 . 那么设计者为什么要这么做呢 ?

    我对 Java 设计模式并不了解 , 因此这里引用 Phith0n师傅 的一段话 , 说的非常形象 .

    这一种比较常见的设计模式 , 被称为 "单例模式 / 工厂模式"

    类似一个 Web 应用 , 数据库连接应该只在服务启动时建立一次 , 而不是每次访问数据库时都建立一个连接 .

    因此开发人员可以把数据库连接写在构造函数中 , 并赋予该函数 "private" 修饰符 . 然后编写一个静态方法来获取该连接 .

    这样 , 只有在类初始化时会调用一次构造函数 , 建立数据库连接 . 后面只需要通过静态方法就能获取数据库连接 , 避免了建立多个数据库链接 .

    我们再看代码 , 发现的确存在一个 getRuntime() 的静态方法 , 并且返回了 java.lang.Runtime 的实例对象 .

    您可能会疑惑 , 这里的构造方法是 Runtime() , 实例化的过程并没有写在构造函数里啊?

    个人认为这是无关紧要的 , 只需要确保实例化过程只进行一次就行了 , 以 Java 反射为例 , 在类初始化时会执行 static{} 代码块中的内容( 详见本文开头 ) , 所以会执行一遍实例化过程 . 由于该过程被赋予了 "private" 修饰符 , 所以后面就再也不能访问它了 . 结果是一样的 .

    这里也引出了 class.newInstance() 方法执行成功的两个关键点 :

    1. 类必须要有无参构造函数 .

    2. 类的构造函数不能是私有的 , 也就是不能通过 "private" 修饰符来修饰构造函数 .

    有了这些结论 , 我们就可以通过 Java 反射机制来执行 exec() 方法了 .

    成功通过 java.lang.Runtime.getRuntime().exec() 调用系统命令 id .

    通过 Method mGetRuntime = cls.getMethod("getRuntime");Method mExec = cls.getMethod("exec",String.class); 分别获取 getRuntime() 方法和 exec() 方法.

    通过 getRuntime()invoke(null) 方法获取 Runtime 实例对象 . 由于调用的是静态方法 , 所以省略 obj 参数 , 由于 getRuntime() 方法没有参数 , 所以这里参数数组为 null .

    通过 exec()invoke(obj , args[]) 方法来执行命令 . 这里 objRuntime 实例对象 , 通过上一步骤获得 , 参数则为系统命令 "id" .

    获取执行结果的字节流 , 将其处理成字符流 , 最后输出字符串 .

  4. 关于 Object obj = mGetRuntime.invoke(null); 这个点需要补充一些东西.

    Phith0n师傅 <<Java安全漫谈 - 反射篇(2)>> 中是这么写的 :

    一开始我并不明白这里 invoke() 的方法参数为什么是一个类( Class ) . 最后完稿时才反应过来 .

    我们可以通过 对象.方法名 来调用实例方法 , 类名.方法名 来调用静态方法 , 那么反过来 , 方法名.invoke(对象) 不就可以映射成 方法名.invoke(类) 嘛 . 事实上 , Phith0n师傅 也是这个意思 , 只不过我基础太差 , 一直没有理解 .


java.lang.ProcessBuilder

上文提到了可以通过 java.lang.Runtime.getRuntime().exec() 方法执行系统命令 , 那么您是否好奇 exec() 方法是如何执行系统命令的呢 ?

直接定位到 java.lang.Runtime类的 exec() 方法 . 这里我们选择最简单的一种重载形式.

该方法返回类对 exec() 方法的调用 , 那么这里返回的 exec() 方法是什么样的呢 ?

cmdarray : 一个字符串数组 , 包含要执行命令及参数 .

envp : 一个字符串数组 , 其中的每个元素都是键值对格式的环境变量 , 如果子进程继承当前进程的环境变量设置 , 则该值为 null.

dir : 一个文件对象 , 代表子进程的工作目录 . 如果子进程继承当前进程的工作目录 , 则该值为 null.

通过代码不难看出 , exec()方法执行命令的原理是通过 ProcessBuilder 对象创建了一个执行命令的子进程 . 那么是否可以直接通过 ProcessBuilder 类来执行命令呢 ? 我们接着看 .

可以看到 , ProcessBuilder 类有两个构造函数 , 这两个函数定义都非常简单 , 即使您没有学过Java , 也能明白 . 一个用于执行没有参数的命令 , 一个用于执行携带参数的命令

需要注意 : command 参数的类型是 List ,

我们并没有指定 envpdir 两个参数的值 , 因此不需要关注 environment()directory() 这两个方法 , 直接来看 start() 方法 .

所以 , 我们只需要调用 java.lang.ProcessBuilder.start() 方法 , 就可以创建子进程来执行命令了 .


执行不带参数的系统命令

通过 class.getConstructor( parameterType ).newInstance("parameter") 来调用含有参数parameter的构造函数

由于 cmdarray 参数的类型是 List , 所以我们执行的命令的类型也必须是 List , 此时可以用 Arrays.asList() 方法将一个可变长参数或者数组转换成 List 类型 .

由于 start() 方法没有参数 , 所以直接调用 Method.invoke(obj) 就可以了


执行携带参数的系统命令

提问 : 那么如何执行携带参数的系统命令呢 ?

其实一共有两种方法来执行携带参数的系统命令 . 下面分别来说一下 .

  1. public ProcessBuilder(List<String> command)

    这里还是通过 Arrays.asList() 方法 . 由于该方法的参数可以是一个可变长参数 , 所以我们可以直接把携带参数的系统命令写到一个数组中 , 然后通过该方法转换成列表 .

    这样就能成功执行携带参数的系统命令了 .


  1. public ProcessBuilder(String... command)

    这是上文所说的 ProcessBuilder 类的第二个构造函数 , 也就是专门用于执行携带参数的系统命令的构造函数 .

    可以看到 , 该构造方法的参数是也是一个可变长参数 . 可变长参数代表着不定长度的参数 , 例如 String ... parameter 代表参数 parameter 由不定个数的字符串组成 .

    关于可变长参数的内容可以参考 Runoob

    Java在编译时会将可变长参数编译成一个数组 . 所以下面两种写法是完全等价的 .

    public void test(String[] names)
    等价于
    public void test(String ... names)

    因此 , 我们可以将 String[].class 作为参数传递给 getConstructor() 方法 , 告诉 ProcessBuilder 调用第二个构造方法来处理携带参数的系统命令 .

    我看 Phith0n 师傅 的文档里是这么写的 .

    事实上 , 我看到很多其他的帖子里也是这么写的 , 但是这么写并不能编译成功 . Javac会抛出异常!

    Warning: non-varargs call of varargs method with inexact argument type for last parameter

    警告: 最后一个参数使用了不精确的变量类型的 varargs 方法的非 varargs 调用

    且无论是执行Linux命令还是Windows命令都会抛出这个错误 .

    其中 varargs 代表可变参数 , 那么这个报错是什么意思呢 ?

    在 Windows 下的 Javac 给出了提示 : 对于 varargs 调用 , 应使用 Object

    于是我们按照下面这种方法来做.

    // 定义一个一维数组实例 , 其中包含了要执行的命令.-
    String[] command = new String[]{"uname","-a"};
    // 创建一个 Object 数组 , 将上一步创建的数组实例的引用赋值给这个 Object 数组
    Object cmds[] = new Object[]{command};

    如果对上面这种写法有疑问 , 可以参考 Java Object数组引用讨论 一文 . 将上面两步合并在一起 , 就变成了下面这一行代码.

    // 这里尝试执行 uname 命令 , 携带参数 -a`
    Object obj = new Object[]{new String[]{"uname","-a"}}

    最终修改后的代码如下所示 :

    这样就能成功执行携带参数的系统命令了 .

    我对上面这种写法还有一些疑问 , 等理解后再进行补充 .


如何调用类的私有方法

这个问题其实之前就遇到过了 , 同样出现在 Phith0n 师傅的文章中 . 我这里也记录一下 .

在讲通过 java.lang.Runtime 执行系统命令时 , 由于该类的构造方法 Runtime() 是一个私有方法 , 所以我们不能调用该方法 , 只能通过 getRuntime() 静态方法来返回一个 Runtime 实例对象 , 然后再调用 exec() 方法 . 为此还提到了 " 单例模式 " 这种设计模式 .

也就是说 , 我们无法直接获取到私有构造方法的 . 那么是否有其他方法来获取私有构造方法呢 ?

java.lang.reflect.AccessibleObject.class 中存在这么一个方法 : setAccessible(boolean flag)

来看下官方文档中是怎么定义这个方法的 .

从中我们可以知道 , 当该方法的参数被设置为 True 时 , 会取消 Java 语言访问检查 , 也就是取消对 public , protected , private 等修饰符的检查 .

但是 , 如果对象是 java.lang.Class.Constructor , 那么将会抛出异常 . 也就是说 , 我们不能通过 getConstructor() 方法来获取构造方法 .

这是为什么呢 ? 其实很好理解 , 与 getMethods()getDeclaredMethods() 一个道理 , getConstructor() 方法只能获取当前类与其继承类的公用( public )方法 . 而不能获取私有方法 .

而我们现在需要获取的方法( Runtime() )是私有方法 , 自然不能通过 getConstructor() 方法来获取 .

那怎么办呢 ? 其实我们可以使用 getDeclaredConstructor() 方法 .

该方法与 getConstructor() 方法最大的不同点在于 : 这个方法会返回指定参数类型的所有构造方法 . 包括 public , protected 以及 private 修饰符修饰的 .

getConstructor() 方法只会返回所有构造方法的一个子集 , 即 public 修饰符修饰的 .

因此 , 通过 getDeclaredConstructor() 方法 , 我们可以获取到私有构造方法 Runtime() . 并且 , 通过setAccessible(boolean flag)关闭 Java 语言访问检查时也不会再抛出异常 .

通过 getDelclaredConstructor() 方法获取到 Runtime() 构造方法 , 关闭 Java 语言访问检查 , 然后构建实例对象 . 最后通过 Method.invoke(obj , parameter) 调用 exec() 方法 .

通过这种方法 , 就可以直接访问任意类的私有构造方法了.


总结

关于 Java 反射的内容就到这了 , 下一章准备看 Java 反序列化漏洞 , 然后再试 RMI利用 , JNDI注入等内容 .

既然开了个大坑 , 就努力把这个坑填完 , 把每个点都理解清楚 . 加油吧 !

本文可以看作是学习笔记 , 由于我没有系统的学过 Java , 所以在某些细节点难免会理解不到位 , 如果您有疑问 , 欢迎留言 .

再次感谢各位前辈的观点和文章 !

最后修改日期:2020年6月28日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。