- 공격자는 서버 사이드 스크립트(ASP, JSP, PHP 등)을 이용하여 웹쉘(WebShell)을 제작 및 업로드
웹쉘(Web Shell) 1. 웹페이지를 뜻하는 "웹(Web)"과 서버에게 명령을 내려 실행하기 위한 인터페이스 역할을 하는 "쉘(Shell)"의 합성어 2. 원격에서 웹서버에 명령어를 실행하기 위하여 만들어진 프로그램 3. 서버 사이드 스크립트(ASP, JSP, PHP 등)를 이용해 제작 4. 취약점을 이용해 웹 서버에 웹쉘 업로드 후 서버상의 정보 유출 및 변조, 악성코드 유포 등의 행위를 수행
- 업로드 파일에 대한 검증이나 환경 설정의 미흡으로 인해 발생할 수 있음
- 해당 공격을 성공하기 위한 조건은 3가지가 있음
공격 성공 조건 1. 파일 업로드가 가능해야 함 2. 파일이 업로드된 디렉터리의 경로를 알아야 함 3. 파일이 업로드된 디렉터리의 실행 권한이 있어야 함
2. 공격실습
2.1 웹쉘 제작
- [캡쳐 1]은 HTTP form에서 입력된 값을 GET 방식으로 전달 받으며, 값이 설정된 경우 쉘 명령어를 수행하는 웹쉘임
2.2 File Upload (Low Level)
- 파일 업로드 기능이 구현된 페이지에 웹쉘 업로드 시도
- 파일 업로드 결과 php 파일이 업로드 되었으며, 파일이 업로드 된 경로 또한 알 수 있음
- 파일 업로드 경로는 /dvwa/hackable/uploads/webshell.php임을 알 수 있음
1. 현재 디렉터리(/dvwa/vulnerabilities/upload) 2. 상위 디렉터리(../)로 2번 이동 후 /hackable/uploads/에 업로드
- 해당 경로로 접근 시 webshell.php 파일을 실행할 수 있으며, 임의의 명령을 수행할 수 있음
2.3 File Upload (Medium Level)
- 웹쉘(PHP) 업로드 시 JPEG나 PNG 파일만 업로드 가능하다는 에러 메시지를 출력함
- 업로드 파일의 type과 size를 검사하여 JPEG와 PNG 파일이 맞는지 확인하여 필터링을 수행
- Content-Type 헤더 변조를 통해 우회할 수 있음
Content-Type : 클라이언트가 서버에 자원을 보낼 때 어떤 유형의 자원을 보내는지 알려주는 헤더
- 버프슈트를 이용해 요청을 인터셉트하여 content-type 헤더를 [캡쳐 8]과 같이 변경
- 변경 후 업로드에 성공한 것을 확인할 수 있음
- 해당 경로에 접근하여 임의의 명령 수행이 가능함
2.4 File Upload (High Level)
- 파일 업로드 시 [캡쳐 5]와 같이 JPEG나 PNG 파일만 업로드 가능하다는 에러 메시지를 출력함
- 업로드 파일명에서 .를 기준으로 나누어 확장자명만을 추출하여, 확장자가 JPG, JPEG, PNG가 맞는지,업로드 파일 사이즈 검증 및 getimagesize함수를 통해 업로드된 파일이 이미지인지 필터링
getimagesize() : 지정된 이미지 파일의 크기를 확인해서 파일타입과 이미지의 크기에 대한 정보를 배열 형태로 출력
- content-type 변조를 통한 우회 방법은 확장자명이 php를 유지하기 때문에 해당 방법은 통하지 않음
- 확장자 검사를 우회하기 위해 확장자명을 .php.jpg로 변경 및 getimagesize() 우회를 위해 GIF89a 추가
GIF89a : GIF 이미지 파일 표준에 정의된 값으로 이미지 파일인 것처럼 속이는 것이 가능
- 버프슈트를 통해 파일명과 GIF89a를 추가하여 전송하면 파일 업로드에 성공함
- 업로드 후 해당 경로에 접근하면 웹쉘이 실행되지 않는데, 이는 업로드한 파일이 jpg 확장자를 가지기 때문임
- [캡쳐 12]에서 디렉터리 이동문자에 대한 필터링이 없으므로, ../를 추가하여 시도해 보았으나 실패함(추가적인 매개변수 등이 있는 것으로 판단됨)
2.5File Upload (Impossible Level)
- Impossible Level의 소스코드를 확인하면 필터링을 2번 수행하는 것을 확인할 수 있음
- 2번의 검증을 통해 php 파일인 웹쉘이 본연의 역할을 수행하지 못하게 됨.
1. 업로드한 파일 이미지인지 확장자, Type, getimagesize함수를 이용해 검증 2. 1차 검증을 통과한 파일의 내용으로 이미지 파일 재생성 - imagecreatefromjpeg() : 파일 또는 URL에서 새 이미지 만들기 - imagejpeg() : 브라우저 또는 파일에 이미지 출력
- HTTP, HTTPS, SSH, 텔넷, FTP, SMTP, POP3, IMAP, Modbus, BACNET, SiemensS7 및 TridiumFox를 지원
- TLS 연결을 수행하고 TCP/IP 4443에서 ZMap이 찾는 모든 호스트의 루트 HTTP 페이지를 수집 가능
ZMap - 인터넷 전체 네트워크 조사를 위해 설계된 고속 단일 패킷 네트워크 스캐너 - 기가 비트로 연결된 컴퓨터에서 Zmap은 전체 공용 IPv4 주소 공간을 45분 이내에 검색할 수 있음 - 10gigE 연결 및 PF_RING으로 ZMap은 5분 만에 IPv4 주소 공간을 스캔할 수
2. 설치
- 다음 명령을 이용해 zgrab2를 설치
$ git clone https://github.com/zmap/zgrab2.git $ cd zgrab2 $ make
- 설치 후 ./zgrab2 --help 명령을 통해 사용법을 알 수 있음
- zgrab 스캐너를 사용하면 User-Agent 헤더에 zgrab이 명시됨
3. 대응
- robots.txt 파일에 zgrab에 대한 접근을 차단
- User-Agent 헤더에 zgrab으로 명시되기 때문에 해당 문자열을 탐지 가능한 패턴 적용 및 IP 차단
- 짧은 시간에 한 IP에서 포트정보만 변경하여 다수의 요청이 발생할 경우 포트스캔을 의심할 수 있음
- 웹 서버의 보안 설정을 통해 기존 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, \, ', "에 역슬래시(\)를 붙여 특수 문자를 이스케이프
- 네트워크를 통한 데이터 통신에 쓰이는 프로토콜인 TLS와 SSL의 오픈 소스 암호화 라이브러리 - C 언어로 작성되어 있는 중심 라이브러리 안에는, 기본적인 암호화 기능 및 여러 유틸리티 함수들이 구현되어 있음
2. HeartBleed
- 클라이언트와 서버는 계속 신호를 주고 받으며 연결상태를 확인하는데 이를 "하트비트(Heartbeat)"라 함
하트비트(Heartbeat)란? 1. 일종의 Echo Request/Echo Reply처럼 서버의 기동 여부를 진단하기 위한 프로토콜 2. 특정 Echo Request를 보내고, 반환 받을 크기를 지정하면 해당 크기만큼 Echo Reply 응답
- '하트비트' 과정 중, 서버가 클라이언트의 요청에 응답할 때 정상적인 응답내용 외 추가정보(중요정보)를 포함한 응답이 전송되어 정보가 노출될 수 있어 HeartBleed로 불림 - 서버가 클라이언트로부터 전달받은 정보의 내용과 그 정보의 길이의 일치 여부를 검증하지 않은 채 응답하여 발생
HeartBleed란? 1. 반환 받을 크기 지정 시 보낸 메세지보다 훨씬 크게 지정(최대 64KB)하여 보낼 수 있는 취약점이 존재 2. 1KB를 보내면서 64KB를 요청 할 경우 나머지 63KB는 메모리에 있는 임의 데이터가 보내짐 3. 위와 같은 동작을 반복하면 메모리상에 흐르는 임의 데이터를 지속적으로 탈취할 수 있음
- 당시 발표에 따르면, 인증 기관에서 인증받은 안전한 웹 서버의 약 17%(약 50만대)가 영향을 받을 수 있었음
3. CVE-2014-0160
- 취약한 버전의 OpenSSL은 공격자의 조작된 요청에 의해 메모리에서 중요한 정보를 얻을 수 있음.
취약한 버전 : OpenSSL 1.0.1 ~ 1.0.1f
원인 : Heartbeat Extension 패킷을 제대로 처리하지 못하여 원격 공격자가 버퍼 오버 읽기를 유발
영향 : 조작된 패킷을 통해 프로세스 메모리에서 중요한 정보를 얻을 수 있음
- [캡쳐 2] 과정을 다음에 비유할 수 있음
1. 엘리스(클라이언트)는 밥(서버)에게 봉투에 100원을 넣고, 1000원이 들었다는 정보와 함께 전송
2. 밥(서버)은 100원을 확인하고, 나머지 900원을 합하여 엘리스(클라이언트)에 응답
3. 엘리스(클라이언트)에게 900원에 해당하는 정보가 유출 됨
4. 실습
4.1) PoC 분석
- PoC는 먼저 서버와 TLS를 이용한 보안 연결을 맺는 것부터 시작하며, 보안 연결이 설정된 이후 s.send(hb) 함수를 통해 Heartbeet 요청을 전송
// 첫 필드는 TLS 레코드가 하트비트임을 명시하고 TLS버전을 알림
18 : TLS record is a heartbeat
03 02 : TLS version 1.1
// 다음으로 하트비트 메시지의 길이와 이 메시지가 하트비트 요청임을 명시
00 03 : Length
01 : Heartbeat request
// 공격의 핵심
// payload길이를 16,384바이트로 표시하고 있지만 그 만큼의 길이에 해당하는 메시지를 보내지 않음
// 16진수로 4000은 16384
40 00 : Payload length(16384bytes)
4.2) 취약점 코드
- 취약한 소스코드를 확인해 보면 사용자 요청 메시지에 대한 길이를 검사하지 않는 것을 확인할 수 있음
4.3) 취약점 실습
- 비박스에서 Heartbleed 취약점을 실습해 볼 수 있으며, HTTPS와 8443포트로 접속해야 함
- 로그인 후 공격 스크립트를 실행시키면 Heartbeat 응답 메세지를 확인할 수 있음.
* 스크립트를 실행하면 결과가 길어 확인이 불편하므로, more 명령으로 내용 확인
- Heartbeet 응답값을 확인하면 로그인정보와 쿠키값이 노출된 것을 볼 수 있으며, 이외에도 추가 정보 유출이 가능함
* 추가정보 : SSL 서버 비밀키, 세션키 등 / 노출되는 정보는 서비스 환경마다 다름
- 해당 패킷을 와이어샤크로 확인하면 [캡쳐 6]과 같음
5. 대응방안
1. 시스템 측면 방안 - 최신 업데이트를 적용 - 사용자 요청 메시지에 대한 길이를 검사하도록 코드 추가
- 업데이트 적용이 어려울 경우 버전 확인 및 Heartbeat 프로토콜 비활성화
1. Open SSL 버전 확인 명령
openssl version -a
2. Open SSL Heartbeat 활성화 여부 확인 명령
// Heartbeat 기능이 활성화되어 있는 경우 heartbeat 문자열이 검색됨
openssl s_client -connect domain.com:443 -tlsextdebug -debug -state | grep -i heartbeat
2. 네트워크 측면 방안 - SSL 서비스 포트에 대해 공격 요청 시 전송되는 |18 03 ??| 탐지 패턴 적용
alert tcp any any < > any SSL 서비스 포트 (content:"|18 03 00|"; depth: 3; content:"|01|"; distance: 2; within: 1; content:!"|00|"; within: 1; msg: "SSLv3 Malicious Heartbleed Request V2”; sid: 1;)
alert tcp any any < > any SSL 서비스 포트 (content:"|18 03 01|"; depth: 3; content:"|01|"; distance: 2; within: 1; content:!"|00|"; within: 1; msg: "TLSv1 Malicious Heartbleed Request V2"; sid: 2;)
alert tcp any any < > any SSL 서비스 포트 (content:"|18 03 02|"; depth: 3; content:"|01|"; distance: 2; within: 1; content:!"|00|"; within: 1; msg: "TLSv1.1 Malicious Heartbleed Request V2"; sid: 3;)
03 00은 SSLv3.0 / 03 01은 TLSv1.0 / 03 02는 TLSv1.1
3. 서비스 관리 측 방안 - 서버 측 SSL 비밀키(Secret Key)가 유출되었을 가능성을 배제할 수 없기 때문에 인증서 재발급 검토 - 취약점에 대한 조치가 완료된 후 사용자들의 비밀번호 재설정을 유도하여 탈취된 계정을 악용한 추가 피해를 방지하는 방안 고려
- 공격자는 취약한 버전의 Apache Struts에 조작된 요청을 전송함으로써 원격 코드를 실행할 수 있음.
- 해당 취약점은 사용자 입력값에 대한 검증이 충분하지 않아 발생하는 취약점.
취약한 버전 : Apache Struts 버전 2.3 ~ 2.3.34 및 2.5 ~ 2.5.16 조건 1. Struts 구성에서 alwaysSelectFullNamespace 플래그가 “true”로 설정됨 (참고: 널리 사용되는 Struts Convention 플러그인을 사용하는 경우 “true”가 기본값으로 설정) 2. Struts 애플리케이션이 특정 namespace를 지정하지 않고 구성되거나 와일드카드 namespace를 이용하는 <action ...> 태그가 포함되어 있음. 결과 웹 응용 프로그램에서 namespace 를 지정하지 않거나 /* 와 같은 와일드카드 namespace 를 사용하는 경우 주어진 작업에 대한 namespace 를 찾을 수 없다면 공격자가 지정한 namespace 를 취하여 OGNL 표현식으로 평가하여 웹 어플리케이션에 원격코드실행을 악용할 수 있음. OGNL(Object-Graph Navigation Language) : Apache Struts의 동작을 사용자 정의하는 데 사용되는 강력한 도메인별 언어