转自:http://blog.nsfocus.net/learning-guide-java-serialization-de-serialization-vulnerability-remediation
JAVA序列化和反序列化是啥?
在现有很多的应用当中,需要对某些对象进行序列化,让它们离开内存空间,入驻物理硬盘,以便可以长期保存,其中最常见的是Web服务器中的Session对象。对象的序列化一般有两种用途:把对象的字节序列永久地保存到硬盘上,通常存放在一个指定文件中;或者在网络上传送对象的字节序列。
而把字节序列恢复为对象的过程称为对象的反序列化。当两个进程在进行远程通信时,彼此可以发送各种类型的数据,而且无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
其实,在不同的计算机语言中,数据结构、对象以及二进制串的表示方式并不相同。对于像Java这种完全面向对象的语言,程序员所操作的一切都是对象,来自于类的实例化。
JAVA序列化和反序列化实例
在Java语言中最接近数据结构的概念,就是 POJO(Plain Old Java Object)或者Javabean。小编更熟悉Java语言,还是以此为例说明一下序列化和反序列化的实现。
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
|
public
static
void
main
(
String
[
]
args
)
throws
Exception
{
SerializeObject
(
)
;
//序列化Object对象
Object
o
=
DeserializeObject
(
)
;
//反序列Object对象
System
.
out
.
println
(
MessageFormat
.
format
(
"name={0},age={1},
sex={2}"
,
o
.
getName
(
)
,
o
.
getSex
(
)
,
o
.
getAge
(
)
,
o
.
getHobby
(
)
)
)
;
}
/**
* MethodName: SerializeObject
* Description: 序列化Object对象
* @author Haom
* @throws FileNotFoundException
* @throws IOException
*/
private
static
void
SerializeObject
(
)
throws
FileNotFoundException
,
IOException
{
Object
object
=
new
Object
(
)
;
object
.
setName
(
"haom"
)
;
object
.
setSex
(
"Female"
)
;
object
.
setAge
(
18
)
;
object
.
setHobby
(
"Taekwondo"
)
;
// 对于ObjectOutputStream 对象输出流,将Object对象存储到M盘的object.txt文件中,完成对Object对象的序列化操作
ObjectOutputStream
oo
=
new
ObjectOutputStream
(
new
FileOutputStream
(
new
File
(
"M:/object.txt"
)
)
)
;
oo
.
writeObject
(
object
)
;
System
.
out
.
println
(
"Object Serialization success!"
)
;
oo
.
close
(
)
;
}
/**
* MethodName: DeserializeObject
* Description: 反序列Object对象
* @author Haom
* @throws Exception
* @throws IOException
*/
private
static
Object
DeserializeObject
(
)
throws
Exception
,
IOException
{
ObjectInputStream
ois
=
new
ObjectInputStream
(
new
FileInputStream
(
new
File
(
"M:/object.txt"
)
)
)
;
Object
object
=
(
Object
)
ois
.
readObject
(
)
;
System
.
out
.
println
(
"Object deserialization success!"
)
;
return
Object
;
}
|
以上代码说明:序列化Object成功后在M盘生成了一个object.txt文件,而反序列化Object是读取M盘的Object.txt后生成了一个Object对象。
当然,并不是一个实现了序列化接口的类的所有字段及属性,都是可以序列化的:
- 如果该类有父类,则分两种情况来考虑:如果该父类已经实现了可序列化接口,则其父类的相应字段及属性的处理和该类相同;如果该类的父类没有实现可序列化接口,则该类的父类所有的字段属性将不会序列化,并且反序列化时会调用父类的默认构造函数来初始化父类的属性,而子类却不调用默认构造函数,而是直接从流中恢复属性的值。
- 如果该类的某个属性标识为static类型的,则该属性不能序列化。
- 如果该类的某个属性采用transient关键字标识,则该属性不能序列化。
那么,在什么情况下,需要自定义序列化的方式? 先举个简单的例子,如下:
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
|
public
class
SeriDemo1
implements
Serializable
{
private
String
name
;
transient
private
String
password
;
// 瞬态,不可序列化状态,该字段的生命周期仅存于调用者的内存中
public
SeriDemo1
(
)
{
}
public
SeriDemo1
(
String
name
,
String
password
)
{
this
.
name
=
name
;
this
.
password
=
password
;
}
//模拟对密码进行加密
private
String
change
(
String
password
)
{
return
password
+
"minna"
;
}
//写入
private
void
writeObject
(
ObjectOutputStream
outStream
)
throws
IOException
{
outStream
.
defaultWriteObject
(
)
;
outStream
.
writeObject
(
change
(
password
)
)
;
}
//读取
private
void
readObject
(
ObjectInputStream
inStream
)
throws
IOException
,
ClassNotFoundException
{
inStream
.
defaultReadObject
(
)
;
String
strPassowrd
=
(
String
)
inStream
.
readObject
(
)
;
//模拟对密码解密
password
=
strPassowrd
.
substring
(
0
,
strPassowrd
.
length
(
)
-
5
)
;
}
//返回一个“以文本方式表示”此对象的字符串
public
String
toString
(
)
{
return
"SeriDemo1 [name="
+
name
+
", password="
+
password
+
"]"
;
}
//静态的main
public
static
void
main
(
String
[
]
args
)
throws
Exception
{
SeriDemo1
demo
=
new
SeriDemo1
(
"haom"
,
"0123"
)
;
ByteArrayOutputStream
buf
=
new
ByteArrayOutputStream
(
)
;
ObjectOutputStream
out
=
new
ObjectOutputStream
(
buf
)
;
out
.
writeObject
(
demo
)
;
ObjectInputStream
in
=
new
ObjectInputStream
(
new
ByteArrayInputStream
(
buf
.
toByteArray
(
)
)
)
;
demo
=
(
SeriDemo1
)
in
.
readObject
(
)
;
System
.
out
.
println
(
demo
)
;
}
}
|
如上代码说明,可以得知,以下情况需要自定义序列化的方式:
- 为了确保序列化的安全性,可以对于一些敏感信息加密;
- 确保对象的成员变量符合正确的约束条件;
- 确保需要优化序列化的性能。
在序列化选型的过程中,安全性的考虑往往发生在跨局域网访问的场景。当通讯发生在公司之间或者跨机房的时候,出于安全的考虑,对于跨局域网的访问往往被限制为基于HTTP/HTTPS的80和443端口。如果使用的序列化协议没有兼容而成熟的HTTP传输层框架支持,可能会导致以下几种结果:
- 因为访问限制而降低服务可用性。
- 被迫重新实现安全协议而导致实施成本升高。
- 开放更多的防火墙端口和协议访问,但是是以牺牲安全性为前提。
反序列化漏洞危害
当应用代码从用户接受序列化数据,并试图反序列化改数据进行下一步处理时,会产生反序列化漏洞,其中最有危害性的就是远程代码注入。
这种漏洞产生原因是,java类ObjectInputStream在执行反序列化时,并不会对自身的输入进行检查,这就说明恶意攻击者可能也可以构建特定的输入,在 ObjectInputStream类反序列化之后会产生非正常结果,利用这一方法就可以实现远程执行任意代码。
这个漏洞的严重风险在于,即使你的代码里没有使用到Apache Commons Collections里的类,只要Java应用的Classpath里有Apache Commons Collections的jar包,都可以远程代码执行。
漏洞的根本问题其实并不是Java序列化的问题,而是Apache Commons Collections允许链式的任意的类函数反射调用。攻击者通过允许Java序列化协议的端口,把攻击代码上传到服务器上,再由Apache Commons Collections里的TransformedMap来执行。
反序列化漏洞补救
现在,Apache Commons Collections在 3.2.2版本中做了一定的安全处理,对这些不安全的Java类的序列化支持增加了开关,默认为关闭状态。
涉及的类包括:CloneTransformer,ForClosure, InstantiateFactory, InstantiateTransformer, InvokerTransformer, PrototypeCloneFactory,PrototypeSerializationFactory, WhileClosure。
RedHat发布JBoss相关产品的解决方案:https://access.redhat.com/solutions/2045023。
严格意义说起来,Java相对来说安全性问题比较少,出现的一些问题大部分是利用反射,最终用Runtime.exec(String cmd)函数来执行外部命令的。如果可以禁止JVM执行外部命令,未知漏洞的危害性会大大降低,可以大大提高JVM的安全性。
比如:
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
|
SecurityManager
originalSecurityManager
=
System
.
getSecurityManager
(
)
;
if
(
originalSecurityManager
==
null
)
{
// 创建自己的SecurityManager
SecurityManager
sm
=
new
SecurityManager
(
)
{
private
void
check
(
Permission
perm
)
{
// 禁止exec
if
(
perm
instanceof
java
.
io
.
FilePermission
)
{
String
actions
=
perm
.
getActions
(
)
;
if
(
actions
!=
null
&
amp
;
&
amp
;
actions
.
contains
(
"execute"
)
)
{
throw
new
SecurityException
(
"execute denied!"
)
;
}
}
// 禁止设置新的SecurityManager
if
(
perm
instanceof
java
.
lang
.
RuntimePermission
)
{
String
name
=
perm
.
getName
(
)
;
if
(
name
!=
null
&
amp
;
&
amp
;
name
.
contains
(
"setSecurityManager"
)
)
{
throw
new
SecurityException
(
"System.setSecurityManager denied!"
)
;
}
}
}
@
Override
public
void
checkPermission
(
Permission
perm
)
{
check
(
perm
)
;
}
@
Override
public
void
checkPermission
(
Permission
perm
,
Object
context
)
{
check
(
perm
)
;
}
}
;
System
.
setSecurityManager
(
sm
)
;
}
|
如上所示,只要在Java代码里简单加一段程序,就可以禁止执行外部程序了。
禁止JVM执行外部命令,是一个简单有效的提高JVM安全性的办法。可以考虑在代码安全扫描时,加强对Runtime.exec相关代码的检测。
小结
本文只是初步进行JAVA序列化和反序列化的科普,让大家对此问题及相关补救方式有个直观的印象和简单了解,下一步深入解还请继续关注绿盟技术博客。使用JAVA反序列化增多了数据的种类,但是还需要尽量避免使用反序列化的交互操作,减少风险的增加。目前,绿盟科技蜂巢社区启动应急机制,已经实现远程代码执行漏洞的在线检测。在社区中,大家可以进行网络安全扫描插件的开发及讨论。
其他相关参考资料: