SQL 인젝션

많은 웹 개발자는 SQL 쿼리가 어떻게 변조될 수 있는지 모르고 SQL 쿼리가 신뢰할 수 있는 명령이라고 가정합니다. 이는 SQL 쿼리가 액세스 제어를 우회하여 표준 인증 및 권한 부여 검사를 우회할 수 있고 때로는 SQL 쿼리가 호스트 운영 체제 수준 명령에 대한 액세스를 허용할 수도 있음을 의미합니다.

직접 SQL 명령 주입은 공격자가 숨겨진 데이터를 노출하거나 중요한 데이터를 무시하거나 데이터베이스 호스트에서 위험한 시스템 수준 명령을 실행하기 위해 기존 SQL 명령을 생성하거나 변경하는 기술입니다. 이것은 애플리케이션이 사용자 입력을 받고 이를 정적 매개변수와 결합하여 SQL 쿼리를 빌드함으로써 수행됩니다. 다음 예는 불행히도 실화를 기반으로 합니다.

입력 유효성 검사가 부족하고 수퍼유저 또는 사용자를 생성할 수 있는 사람을 대신하여 데이터베이스에 연결하기 때문에 공격자는 데이터베이스에 수퍼유저를 생성할 수 있습니다.

예제 #1 결과 집합을 페이지로 분할 ... 및 수퍼유저 만들기(PostgreSQL)

                  
<?php

$offset = $argv[0]; // beware, no input validation!
$query  = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
$result = pg_query($conn, $query);

?>
                  
                

일반 사용자는 $offset이 URL로 인코딩된 '다음', '이전' 링크를 클릭합니다. 스크립트는 들어오는 $offset이 십진수라고 예상합니다. 그러나 누군가가 URL에 다음과 같은 urlencode() 형식을 추가하여 침입하려고 하면 어떻게 될까요?

0;
insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd)
    select 'crack', usesysid, 't','t','crack'
    from pg_shadow where usename='postgres';
--
                

그런 일이 발생하면 스크립트는 그에게 수퍼유저 액세스 권한을 제공합니다. 0;에 유의하십시오. 원래 쿼리에 유효한 오프셋을 제공하고 종료하는 것입니다.

메모: SQL 해석기에서 개발자가 쓴 쿼리의 나머지 부분을 무시하게 하는 일반적인 방법은 --를 붙이는 것이며, 이는 SQL에서 주석 부호입니다.

암호를 얻는 실행 가능한 방법은 검색 결과 페이지를 우회하는 것입니다. 공격자에게 필요한 것은 제출한 변수 중 하나라도 제대로 다뤄지지 않으면서 SQL 구문에 사용되는 것입니다. 이러한 필터는 일반적으로 SELECT 구문에서 WHERE, ORDER BY, LIMIT, OFFSET에 사용됩니다. 데이터베이스가 UNION 구조를 지원하면, 공격자는 원래 질의에 전체 질의를 덧붙여서 임의의 테이블에서 패스워드를 얻을 수 있습니다. 암호화된 암호 필드를 사용하는 것이 좋습니다.

예제 #2 기사 나열 ... 및 일부 암호(모든 데이터베이스 서버)

                  
<?php

$query  = "SELECT id, name, inserted, size FROM products
           WHERE size = '$size'";
$result = odbc_exec($conn, $query);

?>
                  
                

쿼리의 정적 부분은 모든 암호를 표시하는 다른 SELECT 문과 결합할 수 있습니다.

'
union select '1', concat(uname||'-'||passwd) as name, '1971-01-01', '0' from usertable;
--
                

이 질의('--로 다룸)가 $query에서 사용하는 변수 중 하나에 할당되면, 질의 괴물이 깨어납니다. SQL UPDATE도 공격받을 수 있습니다. 이런 질의를 잘라내어서 완전한 새 질의를 덧붙일 수 있습니다. 또한 공격자가 SET 절을 다룰 수도 있습니다. 이 경우 질의를 성공적으로 변경하기 위하여 일부 스키마 정보를 가지고 있어야 합니다. 이는 폼 변수명을 조사하거나, 브루트 포스로 얻을 수 있습니다. 패스워드와 사용자이름을 저장하는 필드의 이름 규칙은 그리 많지 않습니다.

예제 #3 비밀번호 재설정에서 ... 더 많은 권한 얻기까지(모든 데이터베이스 서버)

                  
<?php
$query = "UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';";
?>
                  
                

악의적인 사용자가 $uid' or uid like'%admin'; -- 값을 넣어서 관리자 패스워드를 변경하거나, $pwd에 "hehehe', admin='yes', trusted=100 "(마지막 공백 포함)을 설정하여 권한을 얻을 수도 있습니다. 쿼리가 꼬일 것입니다.

                  
<?php

// $uid: ' or uid like '%admin%
$query = "UPDATE usertable SET pwd='...' WHERE uid='' or uid like '%admin%';";

// $pwd: hehehe', trusted=100, admin='yes
$query = "UPDATE usertable SET pwd='hehehe', trusted=100, admin='yes' WHERE
...;";

?>
                  
                

일부 데이터베이스 호스트에서 운영 체제 수준 명령에 액세스하는 방법에 대한 무서운 예입니다.

예제 #4 데이터베이스 호스트 운영 체제(MSSQL Server) 공격

                  
<?php

$query  = "SELECT * FROM products WHERE id LIKE '%$prod%'";
$result = mssql_query($query);

?>
                  
                

공격자가 $proda%' exec master..xp_cmdshell 'net user test testpass /ADD' -- 값을 제출하면 $query는 다음과 같습니다.

                  
<?php

$query  = "SELECT * FROM products
           WHERE id LIKE '%a%'
           exec master..xp_cmdshell 'net user test testpass /ADD' --%'";
$result = mssql_query($query);

?>
                  
                

MSSQL Server는 로컬 계정 데이터베이스에 새 사용자를 추가하는 명령을 포함하여 일괄 처리에서 SQL 문을 실행합니다. 이 응용 프로그램이 sa로 실행 중이고 MSSQLSERVER 서비스가 충분한 권한으로 실행 중인 경우 공격자는 이제 이 시스템에 액세스할 수 있는 계정을 갖게 됩니다.

메모: 위의 예 중 일부는 특정 데이터베이스 서버에 연결되어 있습니다. 그렇다고 해서 다른 제품에 대해서도 유사한 공격이 불가능한 것은 아닙니다. 귀하의 데이터베이스 서버는 다른 방식으로 유사하게 취약할 수 있습니다.

A worked example of the issues regarding SQL Injection

» xkcd의 이미지 제공


회피 기법

공격자가 성공적인 공격을 수행하려면 데이터베이스 아키텍처에 대한 최소한의 지식이 있어야 한다는 것은 분명하지만 이 정보를 얻는 것은 종종 매우 간단합니다. 예를 들어 데이터베이스가 기본 설치와 함께 공개 소스 또는 기타 공개적으로 사용 가능한 소프트웨어 패키지의 일부인 경우 이 정보는 완전히 공개되어 사용 가능합니다. 이 정보는 인코딩, 난독화 또는 컴파일된 경우에도 비공개 소스 코드에 의해, 그리고 오류 메시지 표시를 통해 사용자 고유의 코드에 의해 공개될 수도 있습니다. 다른 방법에는 공통 테이블 및 열 이름의 사용자가 포함됩니다. 예를 들어, 열 이름이 'id', 'username' 및 'password'인 'users' 테이블을 사용하는 로그인 양식입니다.

이러한 공격은 주로 보안을 염두에 두고 작성되지 않은 코드를 악용하는 것을 기반으로 합니다. 선택 상자, 숨겨진 입력 필드 또는 쿠키에서 가져온 것이라 하더라도 모든 종류의 입력, 특히 클라이언트 측에서 오는 입력을 절대 신뢰하지 마십시오. 첫 번째 예는 그러한 무책임한 쿼리가 재앙을 일으킬 수 있음을 보여줍니다.

  • 수퍼유저나 데이터베이스 소유자로 데이터베이스에 연결하지 마십시오. 매우 제한된 권한으로 항상 사용자 정의된 사용자를 사용하십시오.
  • 바운드 변수와 함께 준비된 명령문을 사용하십시오. PDO, MySQLi 및 기타 라이브러리에서 제공합니다.
  • 주어진 입력에 예상 데이터 유형이 있는지 확인하십시오. PHP변수 함수문자 유형 함수(예: 각각 is_numeric(), ctype_digit())에서 볼 수 있는 가장 단순한 것부터 Perl 호환 정규식 지원에 이르기까지 광범위한 입력 유효성 검사 기능을 가지고 있습니다.
  • 응용 프로그램이 숫자 입력을 기다리는 경우 ctype_digit()을 사용하여 데이터를 확인하거나 settype()을 사용하여 유형을 자동으로 변경하거나 sprintf()로 숫자 표현을 사용하십시오.

예제 #5 페이징을 위한 쿼리를 작성하는 보다 안전한 방법

                  
<?php

settype($offset, 'integer');
$query = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";

// please note %d in the format string, using %s would be meaningless
$query = sprintf("SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET %d;",
                 $offset);

?>
                  
                
  • 데이터베이스 계층이 바인딩 변수를 지원하지 않는 경우 데이터베이스별 문자열 이스케이프 함수(예: mysql_real_escape_string(), sqlite_escape_string() 등)로 데이터베이스에 전달되는 숫자가 아닌 사용자 제공 값을 인용하십시오. addlashes()와 같은 일반 함수는 매우 특정한 환경에서만 유용하므로(예: NO_BACKSLASH_ESCAPES가 비활성화된 단일 바이트 문자 집합의 MySQL) 사용하지 않는 것이 좋습니다.
  • 공정한 수단이나 부정한 방법으로 데이터베이스 특정 정보, 특히 스키마에 대한 정보를 인쇄하지 마십시오. 오류 보고오류 처리 및 로깅 함수도 참조하십시오.
  • 저장 프로시저와 이전에 정의된 커서를 사용하여 사용자가 테이블이나 뷰에 직접 액세스하지 않도록 데이터 액세스를 추상화할 수 있지만 이 솔루션에는 또 다른 영향이 있습니다.

이 외에도 스크립트 내에서 또는 로깅을 지원하는 경우 데이터베이스 자체에서 쿼리를 로깅할 수 있습니다. 분명히 로깅은 유해한 시도를 막을 수는 없지만 우회한 응용 프로그램을 추적하는 데 도움이 될 수 있습니다. 로그는 그 자체로는 유용하지 않지만 포함된 정보를 통해 유용합니다. 일반적으로 세부 사항이 적을수록 좋습니다.