漏洞说明
Apache Ofbiz 0day 未授权代码执行(CVE-2023-49070):影响Apache OFBiz before 18.12.10,来自于CVE-2020-9496的绕过
漏洞复现
漏洞分析
漏洞触发异常调用栈如下
getResult:33, SerializableParser (org.apache.xmlrpc.parser)
endValueTag:78, RecursiveTypeParserImpl (org.apache.xmlrpc.parser)
endElement:185, MapParser (org.apache.xmlrpc.parser)
endElement:103, RecursiveTypeParserImpl (org.apache.xmlrpc.parser)
endElement:165, XmlRpcRequestParser (org.apache.xmlrpc.parser)
endElement:-1, AbstractSAXParser (org.apache.xerces.parsers)
scanEndElement:-1, XMLNSDocumentScannerImpl (org.apache.xerces.impl)
dispatch:-1, XMLDocumentFragmentScannerImpl$FragmentContentDispatcher (org.apache.xerces.impl)
scanDocument:-1, XMLDocumentFragmentScannerImpl (org.apache.xerces.impl)
parse:-1, XML11Configuration (org.apache.xerces.parsers)
parse:-1, XML11Configuration (org.apache.xerces.parsers)
parse:-1, XMLParser (org.apache.xerces.parsers)
parse:-1, AbstractSAXParser (org.apache.xerces.parsers)
parse:-1, SAXParserImpl$JAXPSAXParser (org.apache.xerces.jaxp)
getRequest:65, XmlRpcStreamServer (org.apache.xmlrpc.server)
execute:199, XmlRpcStreamServer (org.apache.xmlrpc.server)
invoke:137, XmlRpcEventHandler (org.apache.ofbiz.webapp.event)
runEvent:733, RequestHandler (org.apache.ofbiz.webapp.control)
doRequest:454, RequestHandler (org.apache.ofbiz.webapp.control)
doGet:210, ControlServlet (org.apache.ofbiz.webapp.control)
doPost:85, ControlServlet (org.apache.ofbiz.webapp.control)
service:707, HttpServlet (javax.servlet.http)
service:790, HttpServlet (javax.servlet.http)
internalDoFilter:292, ApplicationFilterChain (org.apache.catalina.core)
doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
doFilter:209, ContextFilter (org.apache.ofbiz.webapp.control)
internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
doFilter:156, ControlFilter (org.apache.ofbiz.webapp.control)
internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
invoke:212, StandardWrapperValve (org.apache.catalina.core)
invoke:106, StandardContextValve (org.apache.catalina.core)
invoke:502, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:141, StandardHostValve (org.apache.catalina.core)
invoke:79, ErrorReportValve (org.apache.catalina.valves)
invoke:88, StandardEngineValve (org.apache.catalina.core)
invoke:616, AbstractAccessLogValve (org.apache.catalina.valves)
service:528, CoyoteAdapter (org.apache.catalina.connector)
process:1100, AbstractHttp11Processor (org.apache.coyote.http11)
process:687, AbstractProtocol$AbstractConnectionHandler (org.apache.coyote)
doRun:1520, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:1476, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)
(1)触发反序列化
在getResult:33, SerializableParser (org.apache.xmlrpc.parser)中调用ois.readObject()触发反序列化漏洞
getRequest:65, XmlRpcStreamServer (org.apache.xmlrpc.server) 这里能看到直接将请求流中的数据交给了XMLReader进行处理
当查询到serializable标签的时候,会调用SerializableParser.startElement()
通过java的子父类关系,最终调用ByteArrayParser.startElement(),会对内容进行base64的解码。
(2)标签处理
XmlRpcRequestParser.java中的startElement选择标签进行处理。当标签不匹配则调用super.startElement(pURI, pLocalName, pQName, pAttrs);就是RecursiveTypeParserImpl.startElement
public void startElement(String pURI, String pLocalName, String pQName,
Attributes pAttrs) throws SAXException {
switch (level++) {
case 0:
if (!"".equals(pURI) || !"methodCall".equals(pLocalName)) {
throw new SAXParseException("Expected root element 'methodCall', got "
+ new QName(pURI, pLocalName),
getDocumentLocator());
}
break;
case 1:
if (methodName == null) {
if ("".equals(pURI) && "methodName".equals(pLocalName)) {
inMethodName = true;
} else {
throw new SAXParseException("Expected methodName element, got "
+ new QName(pURI, pLocalName),
getDocumentLocator());
}
} else if (params == null) {
if ("".equals(pURI) && "params".equals(pLocalName)) {
params = new ArrayList();
} else {
throw new SAXParseException("Expected params element, got "
+ new QName(pURI, pLocalName),
getDocumentLocator());
}
} else {
throw new SAXParseException("Expected /methodCall, got "
+ new QName(pURI, pLocalName),
getDocumentLocator());
}
break;
case 2:
if (!"".equals(pURI) || !"param".equals(pLocalName)) {
throw new SAXParseException("Expected param element, got "
+ new QName(pURI, pLocalName),
getDocumentLocator());
}
break;
case 3:
if (!"".equals(pURI) || !"value".equals(pLocalName)) {
throw new SAXParseException("Expected value element, got "
+ new QName(pURI, pLocalName),
getDocumentLocator());
}
startValueTag();
break;
default:
super.startElement(pURI, pLocalName, pQName, pAttrs);
break;
}
}
TypeFactoryImpl.getParser() 这里puri正确的时候,会通过不同的标签取到不同的parser()
public TypeParser getParser(XmlRpcStreamConfig pConfig, NamespaceContextImpl pContext, String pURI, String pLocalName) {
if (XmlRpcWriter.EXTENSIONS_URI.equals(pURI)) {
if (!pConfig.isEnabledForExtensions()) {
return null;
}
if (NullSerializer.NIL_TAG.equals(pLocalName)) {
return new NullParser();
} else if (I1Serializer.I1_TAG.equals(pLocalName)) {
return new I1Parser();
} else if (I2Serializer.I2_TAG.equals(pLocalName)) {
return new I2Parser();
} else if (I8Serializer.I8_TAG.equals(pLocalName)) {
return new I8Parser();
} else if (FloatSerializer.FLOAT_TAG.equals(pLocalName)) {
return new FloatParser();
} else if (NodeSerializer.DOM_TAG.equals(pLocalName)) {
return new NodeParser();
} else if (BigDecimalSerializer.BIGDECIMAL_TAG.equals(pLocalName)) {
return new BigDecimalParser();
} else if (BigIntegerSerializer.BIGINTEGER_TAG.equals(pLocalName)) {
return new BigIntegerParser();
} else if (SerializableSerializer.SERIALIZABLE_TAG.equals(pLocalName)) {
return new SerializableParser();
} else if (CalendarSerializer.CALENDAR_TAG.equals(pLocalName)) {
return new CalendarParser();
}
} else if ("".equals(pURI)) {
if (I4Serializer.INT_TAG.equals(pLocalName) || I4Serializer.I4_TAG.equals(pLocalName)) {
return new I4Parser();
} else if (BooleanSerializer.BOOLEAN_TAG.equals(pLocalName)) {
return new BooleanParser();
} else if (DoubleSerializer.DOUBLE_TAG.equals(pLocalName)) {
return new DoubleParser();
} else if (DateSerializer.DATE_TAG.equals(pLocalName)) {
return new DateParser(new XmlRpcDateTimeDateFormat(){
private static final long serialVersionUID = 7585237706442299067L;
protected TimeZone getTimeZone() {
return controller.getConfig().getTimeZone();
}
});
} else if (ObjectArraySerializer.ARRAY_TAG.equals(pLocalName)) {
return new ObjectArrayParser(pConfig, pContext, this);
} else if (MapSerializer.STRUCT_TAG.equals(pLocalName)) {
return new MapParser(pConfig, pContext, this);
} else if (ByteArraySerializer.BASE_64_TAG.equals(pLocalName)) {
return new ByteArrayParser();
} else if (StringSerializer.STRING_TAG.equals(pLocalName)) {
return new StringParser();
}
}
return null;
}
(3)poc构造
根据上面的XmlRpcRequestParser.startElement标签判断顺序,我们可以列一个简易的poc模版
<methodCall>
<methodName>22</methodName>
<params>
<param>
<value>
</value>
</param>
</params>
</methodCall>
当我们直接尝试用serializable,发现报错了,需要prefix不为空。
通过代码发现,需要对cachedPrefix赋值不为空,也就是这里typeParser不为空,然后走到context.endPrefixMapping(pPrefix);
选用struct
其内部定义标签格式如下
汇总payload格式如下
<?xml version="1.0">
<methodCall>
<methodName>
<params>
<param>
<value>
<struct>
<member>
<name>***</name>
<value>
<serializable xmlns="http://ws.apache.org/xmlrpc/namespaces/extensions">
base64的反序列化数据
</serializable>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>
(4)绕过鉴权
LoginWorker.checkLogin()
public static String checkLogin(HttpServletRequest request, HttpServletResponse response) {
GenericValue userLogin = checkLogout(request, response);
// have to reget this because the old session object will be invalid
HttpSession session = request.getSession();
String username = null;
String password = null;
if (userLogin == null) {
// check parameters
username = request.getParameter("USERNAME");
password = request.getParameter("PASSWORD");
// check session attributes
if (username == null) username = (String) session.getAttribute("USERNAME");
if (password == null) password = (String) session.getAttribute("PASSWORD");
//in this condition log them in if not already; if not logged in or can't log in, save parameters and return error
if ((username == null) || (password == null) || ("error".equals(login(request, response)))) {
// make sure this attribute is not in the request; this avoids infinite recursion when a login by less stringent criteria (like not checkout the hasLoggedOut field) passes; this is not a normal circumstance but can happen with custom code or in funny error situations when the userLogin service gets the userLogin object but runs into another problem and fails to return an error
request.removeAttribute("_LOGIN_PASSED_");
// keep the previous request name in the session
session.setAttribute("_PREVIOUS_REQUEST_", request.getPathInfo());
// NOTE: not using the old _PREVIOUS_PARAMS_ attribute at all because it was a security hole as it was used to put data in the URL (never encrypted) that was originally in a form field that may have been encrypted
// keep 2 maps: one for URL parameters and one for form parameters
Map<String, Object> urlParams = UtilHttp.getUrlOnlyParameterMap(request);
if (UtilValidate.isNotEmpty(urlParams)) {
session.setAttribute("_PREVIOUS_PARAM_MAP_URL_", urlParams);
}
Map<String, Object> formParams = UtilHttp.getParameterMap(request, urlParams.keySet(), false);
if (UtilValidate.isNotEmpty(formParams)) {
session.setAttribute("_PREVIOUS_PARAM_MAP_FORM_", formParams);
}
if (Debug.infoOn()) Debug.logInfo("checkLogin: PathInfo=" + request.getPathInfo(), module);
return "error";
}
}
return "success";
}
如果(login(request,response)返回不为error则绕过鉴权,username和password随便写就行
这里可以通过传入requirePasswordChange=Y
这里我用的老版本鉴权接口判定
还存在路径绕过,由于本地没有环境复现,参考代码,采用危险的geturi,可以通过; ../的方式截断,导致权限绕过。
原文始发于微信公众号(e0m安全屋):HTTP_Apache_Ofbiz_反序列化漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论