SQL 주입 공격(Injection Attack)을 사전에 차단하는 방법

일상속으로 Go~Go! 2007. 1. 4. 02:11
데이터 보안

SQL 주입 공격을 사전에 차단하는 방법



문서의 주제:
  • SQL 주입 공격(Injection Attack)의 작동 방식
  • 취약점 점검 테스트
  • 사용자 입력 확인
  • 공격 차단을 위한 .NET 기능 활용
  • 예외 처리의 중요성
본 문서에서 활용된 기술:
ASP.NET, C#, SQL


코드 다운로드:
SQLInjection.exe (153KB)

ASP.NET과 같은 고급 서버측 기술과 Microsoft SQL Server와 같은 강력한 데이터베이스 서버를 갖춤으로써 개발자들은 너무나 쉽게 동적 데이터 중심 웹 사이트를 만들 수 있게 되었습니다. 그러나 ASP.NET과 SQL의 위력은 SQL 주입 공격과 같은 너무나 흔한 부류의 공격을 감행하는 수단으로 해커에 의해 쉽게 활용될 수 있습니다. SQL 주입 공격의 기본 발상은 다음과 같습니다. 입력란에 사용자가 텍스트를 입력하면 이를 사용하여 데이터베이스에 대해 쿼리를 실행하도록 구성된 웹 페이지가 있습니다. 그런데 해커가 백 엔드 데이터베이스에 침입하거나 내용을 변경 또는 손상시키는 데 사용될 수 있도록 쿼리의 특성을 바꿔 놓는 잘못된 SQL 문을 이 입력란에 입력합니다. 어떻게 이러한 일이 가능할까요? 한 가지 예를 들어 설명하도록 하겠습니다.


정상적인 SQL의 변질

많은 ASP.NET 응용 프로그램들이 그림 1과 같은 폼을 사용하여 사용자를 인증합니다. 사용자가 BadLogin.aspx의 Login 단추를 클릭하면 cmdLogin_Click 메서드는 이 사용자가 폼의 입력란 컨트롤에 입력한 값과 UserName 및 Password가 일치하는 Users 테이블의 레코드 수를 계산하는 쿼리를 실행하여 사용자 인증을 시도합니다.

이 폼은 대부분의 경우 정확히 의도대로 작동합니다. 사용자가 Users 테이블의 레코드와 일치하는 사용자 이름 및 암호를 입력하면 동적으로 생성된 SQL 쿼리가 사용되어 일치하는 행의 수를 검색합니다. 그러면 이 사용자는 인증되어 요청한 페이지로 리디렉션됩니다. 잘못된 사용자 이름 및/또는 암호를 입력한 사용자는 인증되지 않습니다. 그러나 여기서 해커가 다음과 같이 해가 없어 보이는 텍스트를 UserName 입력란에 입력하면 올바른 사용자 이름과 암호를 몰라도 시스템에 진입할 수 있습니다.

' Or 1=1 --
해커는 잘못된 SQL을 쿼리에 주입하여 시스템에 침입합니다. 이 해킹 수법이 통하는 이유는 실행된 쿼리가 아래처럼 사용자가 입력한 값과 고정 문자열의 연결에 의해 구성되기 때문입니다.
string strQry = "SELECT Count(*) FROM Users WHERE UserName='" +
    txtUser.Text + "' AND Password='" + txtPassword.Text + "'";
사용자가 올바른 사용자 이름과 암호인 "Paul"과 "password"를 입력하는 경우 strQry는 다음과 같습니다.
SELECT Count(*) FROM Users WHERE UserName='Paul' AND Password='password'
그러나 해커가 다음과 같이 입력하면
Or 1=1 --
쿼리는 다음과 같이 구성됩니다
SELECT Count(*) FROM Users WHERE UserName='' Or 1=1 --' AND Password=''

하이픈 쌍은 SQL에서 주석의 시작을 가리키므로 이 쿼리는 간단히 다음과 같이 됩니다.

SELECT Count(*) FROM Users WHERE UserName='' Or 1=1
식 1=1은 테이블의 모든 행에 대해 항상 true이므로 true 식이 다른 식과 or로 연결되면 항상 true를 반환합니다. 따라서 Users 테이블에 행이 최소한 하나 있다고 가정하면 언제나 이 SQL은 0이 아닌 레코드 수를 반환합니다

모든 SQL 주입 공격이 폼 인증과 관련되는 것은 아닙니다. 동적으로 구성된 SQL과 신뢰할 수 없는 사용자 입력이 있는 응용 프로그램만 있으면 됩니다. 조건이 적절할 경우, 그러한 공격이 어느 정도의 손상을 발생시키느냐는 해당 해커가 SQL 언어 및 데이터베이스 구성을 어느 정도 알고 있느냐에 달려 있습니다.

다음으로 BadProductList.aspx에서 발췌한 그림 2의 코드를 살펴보겠습니다. 이 페이지에는 Northwind 데이터베이스의 제품들이 표시되며 사용자가 txtFilter라는 입력란을 사용하여 제품 목록을 필터링할 수 있게 되어 있습니다. 앞의 예와 마찬가지로 이 페이지 역시 SQL 주입 공격의 대상이 되기에 충분합니다. 이는 실행된 SQL이 사용자가 입력한 값으로 동적으로 구성되기 때문입니다. 특히 이런 페이지는 그야말로 해커의 천국입니다. 영리한 해커라면 이를 사용하여 기밀 정보를 노출시키고, 데이터베이스의 데이터를 변경하며, 데이터베이스 레코드를 손상시키고, 새 데이터베이스 사용자 계정을 만들 수도 있기 때문입니다.

SQL Server를 비롯한 대부분의 SQL 호환 데이터베이스는 sysobjects, syscolumns, sysindexes 등의 이름을 가진 일련의 시스템 테이블에 메타데이터를 저장합니다. 이것은 해커가 시스템 테이블을 사용하여 데이터베이스의 스키마 정보를 확인함으로써 나중에 그 데이터베이스를 위험에 빠뜨리는 데 활용할 수 있다는 뜻입니다. 예를 들어 아래와 같은 텍스트를 txtFilter 입력란에 입력하면 데이터베이스에 있는 사용자 테이블의 이름을 노출하는 데 사용될 수 있습니다.

' UNION SELECT id, name, '', 0 FROM sysobjects WHERE xtype ='U' --

특히 UNION 문은 한 쿼리의 결과를 다른 것에 접합할 수 있게 한다는 점에서 해커에게 특히 유용합니다. 이 경우 해커는 데이터베이스에 있는 사용자 테이블의 이름을 Products 테이블의 원래 쿼리에 접합시켰습니다. 여기서 유일한 트릭은 해당 열의 수와 데이터 형식을 원래의 쿼리에 대응시키는 것입니다. 이전의 쿼리를 통해 이 데이터베이스에 Users라는 이름의 테이블이 있다는 것이 드러날 것입니다. 두 번째 쿼리에서는 Users 테이블의 열이 드러날 것입니다. 이 정보를 활용하여 해커는 txtFilter 입력란에 다음을 입력할 수 있습니다.

' UNION SELECT 0, UserName, Password, 0 FROM Users --
이 쿼리를 입력하면 그림 3에 나타나는 것처럼 Users 테이블에 있는 사용자 이름과 암호가 드러납니다.
Figure 3 Querying the Users Table
그림 3  Users 테이블에 대한 쿼리

SQL 주입 공격은 데이터를 변경하거나 데이터베이스를 손상시키는 데에도 사용될 수 있습니다. SQL 주입 해커는 txtFilter 입력란에 다음과 같이 입력하여 첫 번째 제품의 가격을 18달러에서 0.01달러로 변경한 후 다른 사람이 알기 전에 신속하게 해당 제품 몇 개를 구입할 수 있습니다.

'; UPDATE Products SET UnitPrice = 0.01 WHERE ProductId = 1--

이 해킹 방식이 통하는 이유는 SQL Server에서는 여러 SQL 문을 세미콜론이나 공백으로 구분하여 같은 문자열에 넣을 수 있기 때문입니다. 이 예에서 DataGrid는 아무 것도 표시하지 않지만 업데이트 쿼리는 성공적으로 실행됩니다. 이와 같은 기법을 사용하여 DROP TABLE 문을 실행하거나 시스템 저장 프로시저를 실행하여 새로운 사용자 계정을 만들고 그 사용자를 sysadmin 역할에 추가할 수도 있을 것입니다. 이러한 모든 해킹이 그림 2의 BadProductList.aspx 페이지를 사용하여 수행될 수 있는 것입니다.


동일한 해킹 위험성

SQL 주입 공격이 SQL Server에만 한정되지 않는다는 점을 인식하는 것이 중요합니다. Oracle, MySQL, DB2, Sybase 등을 비롯한 다른 데이터베이스들도 이러한 유형의 공격에 노출될 수 있습니다. SQL 주입 공격이 가능한 것은 SQL 언어에 포함되어 이를 아주 강력하고 유연하게 만들어주는 여러 기능들 때문입니다. 이러한 기능을 나열하면 다음과 같습니다.

  • 하이픈 쌍을 사용하여 SQL 문에 주석을 포함시킬 수 있는 기능
  • 여러 SQL 문을 같은 문자열에 묶어 이를 일괄 처리로 실행할 수 있는 기능
  • SQL을 사용하여 표준 시스템 테이블 집합로부터 메타데이터를 쿼리할 수 있는 기능

일반적으로 데이터베이스에서 지원하는 SQL 언어가 강력할수록 그 데이터베이스는 공격에 더 취약합니다. 따라서 SQL Server가 주입 공격의 대상으로 널리 사용되는 것은 놀라운 일이 아닙니다.

SQL 주입 공격은 ASP.NET 응용 프로그램에만 한정되는 것이 아닙니다. 고전적인 ASP, Java, JSP 및 PHP 응용 프로그램도 똑같이 위험합니다. 사실 SQL 주입 공격은 데스크톱 응용 프로그램에도 영향을 미칠 수 있습니다. 예를 들어 필자는 본 문서를 위한 다운로드 파일에 SQLInjectWinForm이라는 이름의 Windows Forms 응용 프로그램 샘플을 포함시켜 뒀는데 이것 역시 SQL 주입 공격에 노출될 수 있습니다.

SQL 주입 공격 차단에 사용할 수 있는 주요 수단 1~2가지를 쉽게 지적할 수 있겠으나, 이 문제에는 계층적인 접근 방식을 취하는 것이 최선입니다. 계층적인 방식을 취한다면 일부 취약점 때문에 차단 수단 중 하나가 무너지는 상황에서도 보호 체계는 유지될 수 있습니다. 그림 4에는 권장할 만한 계층적 수단들이 요약되어 있습니다.


모든 입력을 경계할 것

그림 4 에 제시된 첫 번째 원칙은 극히 중요합니다. 모든 입력을 경계하십시오! 확인되지 않은 사용자 입력을 데이터베이스 쿼리에 절대 사용해서는 안 됩니다. ASP.NET 유효성 검사 컨트롤, 특히 RegularExpressionValidator 컨트롤은 사용자 입력의 유효성을 검사하는 데 유용한 툴입니다.

유효성 검사에 대한 기본적인 접근 방식은 두 가지입니다. 문제성 있는 문자의 사용을 금지하거나 필요한 문자 일부만 허용하는 것입니다. 하이픈이나 작은따옴표와 같은 일부 문제성 있는 문자를 금지하는 것은 쉽지만 이 방식은 두 가지 이유에서 최적의 방식은 아닙니다. 첫째, 해커에게 유용한 문자를 하나라도 놓칠 수 있습니다. 둘째, 악성 문자를 표현하는 방식이 여러 가지인 경우가 많습니다. 예를 들어 해커가 작은따옴표를 이스케이프 처리하면 유효성 검사 코드가 이것을 놓쳐 이 이스케이프 처리된 작은따옴표를 데이터베이스로 그냥 보낼 수 있는데 데이터베이스에서는 이것을 일반적인 작은따옴표 문자와 똑같이 취급할 수 있습니다. 더 나은 방법은 허용 가능한 문자를 정해서 그 문자만 허용하는 방식입니다. 이 방식에는 더 많은 작업이 필요하지만 입력에 대해 훨씬 강한 제어가 확보되고 더 안전합니다. 이 가운데 어떤 방식을 취했든 입력의 길이 역시 제한하는 것이 좋습니다. 일부 해킹에서는 많은 수의 문자가 사용되기 때문입니다.

GoodLogin.aspx(역시 코드 다운로드에 포함됨)에는 각각 사용자 이름과 암호를 위한 두 개의 정규 표현식 검사기 컨트롤이 입력의 길이를 숫자, 알파벳 문자, 밑줄을 포함하여 4~12자로 제한하는 다음과 같은 ValidationExpression 값과 함께 포함되어 있습니다.

[\d_a-zA-Z]{4,12}

해를 일으킬 가능성이 있는 문자를 사용자가 입력란에 입력할 수 있도록 허용할 필요가 있을 수도 있습니다. 예를 들어 사람 이름의 일부로 작은따옴표(또는 아포스트로피)를 입력해야 할 수도 있습니다. 이러한 경우에는 작은따옴표의 각 인스턴스를 큰따옴표로 바꿔주는 정규 표현식 또는 String.Replace 메서드를 사용하여 작은따옴표가 해를 일으키지 않게 만들 수 있습니다. 예를 들면 다음과 같습니다.

string strSanitizedInput = strInput.Replace("'", "''");

동적 SQL 사용 금지

필자가 본 문서에서 설명한 SQL 주입 공격은 모두 동적 SQL, 즉 사용자가 입력한 값과 SQL을 연결하여 구성된 SQL 문의 실행에 의존하고 있습니다. 그러나 매개 변수화된 SQL을 사용하면 해커가 여러분의 코드에 SQL을 주입할 수 있는 여지가 크게 줄어듭니다.

그림 5 의 코드에서는 주입 공격을 차단하기 위해 매개 변수화된 SQL을 사용하고 있습니다. 여러분이 반드시 임의 SQL을 사용해야 한다면 매개 변수화된 SQL은 훌륭한 처방입니다. 여러분의 IT 부서에서 저장 프로시저를 신뢰하지 않는다거나 버전 5.0까지는 이를 지원하지 않았던 MySQL과 같은 제품을 사용한다면 이 방식이 필수적일 것입니다. 그러나 가능하다면 저장 프로시저를 채택하여 데이터베이스의 기본 테이블에 대한 모든 허용을 제거하는 기능을 추가함으로써 그림 3의 경우와 같은 쿼리를 만들 수 없도록 하는 것이 좋습니다. 그림 6의 BetterLogin.aspx에서는 저장 프로시저인 procVerifyUser를 사용하여 사용자를 확인하고 있습니다.


최소 권한 실행

BadLogin.aspx와 BadProductList.aspx에서 확인된 나쁜 방법 중 하나는 sa 계정을 사용하는 연결 문자열을 사용하는 것입니다. 아래는 Web.config에서 볼 수 있는 연결 문자열입니다.

<add key="cnxNWindBad"
value="server=localhost;uid=sa;pwd=;database=northwind;" />

이 계정은 시스템 관리자 역할을 수행하므로 글자 그대로 모든 작업(로그인 만들기 및 데이터베이스 삭제 등)을 수행할 수 있습니다. 따라서 응용 프로그램 데이터베이스 액세스에 sa(또는 높은 권한을 가진 계정)를 사용하는 것은 아주 좋지 않다라고만 말해 두겠습니다. 제한된 액세스 계정을 만들어 이를 대신 사용하는 것이 훨씬 좋습니다. GoodLogin.aspx에 사용된 계정은 다음과 같은 연결 문자열을 사용하고 있습니다.

<add key="cnxNWindGood"
value="server=localhost;uid=NWindReader;pwd=utbbeesozg4d;
database=northwind;" />

NWindReader 계정은 데이터베이스의 테이블을 읽는 것으로만 액세스가 제한된 db_datareader 역할로 실행되고 있습니다. BetterLogin.aspx는 저장 프로시저, 그리고 이 저장 프로시저를 실행할 수 있는 권한 외에는 이 테이블에 대해 어떤 권한도 갖고 있지 않은 WebLimitedUser라는 로그인을 사용함으로써 상황을 더욱 개선하고 있습니다.


암호는 안전하게 저장할 것

그림 3에 제시된 SQL 주입 공격은 Users 테이블로부터 사용자 이름과 암호를 노출시켰습니다. 이런 종류의 테이블은 폼 인증을 사용했을 때, 그리고 암호가 일반 텍스트로 저장되는 많은 응용 프로그램에서 흔히 사용됩니다. 더 나은 방법은 암호화되거나 해시된 암호를 데이터베이스에 저장하는 것입니다. 해시된 암호는 암호가 해독될 수 없으므로 암호화된 암호보다 더 안전합니다. 해시에 솔트(암호화 방식으로 보호되는 임의의 값)를 추가하면 해시된 암호를 더욱 견고하게 만들 수 있습니다. BestLogin.aspx에는 사용자가 입력한 암호를 SecureUsers 테이블에 저장된 암호의 솔트된 해시 버전과 비교하는 코드가 들어 있습니다(그림 7 참조). 해시 퍼즐의 나머지 조각은 AddSecureUser.aspx입니다. 이 페이지는 솔트된 해시된 암호를 생성하여 SecureUsers 테이블에 저장하는 데 사용될 수 있습니다

BestLogin.aspx 및 AddSecureUser.aspx는 모두 그림 8과 같이 SaltedHash 클래스 라이브러리의 코드를 사용합니다. Jeff Prosise가 만든 이 코드는 System.Web.Security 네임스페이스로부터 FormsAuthentication.HashPasswordForStoringInConfigFile 메서드를 사용하여 암호 해시를 만들고 System.Security.Cryptography 네임스페이스로부터 RNGCryptoServiceProvider.GetNonZeroBytes 메서드를 사용하여 임의의 16바이트 솔트 값을 생성합니다(이것은 Convert.ToBase64String을 사용하여 문자열로 변환하면 24자가 됩니다).

SQL 주입 공격과 직접적인 관계는 없지만, BestLogin.aspx에는 또 다른 보안 최적 방법으로서 연결 문자열의 암호화 방식이 제시되어 있습니다. BestLogin.aspx의 경우처럼 포함된 데이터베이스 계정 암호가 들어 있는 경우에는 연결 문자열의 보안이 특히 중요합니다. 데이터베이스에 연결하려면 연결 문자열의 암호가 해독된 버전이 필요하기 때문에 연결 문자열을 해시할 수 없습니다. 그 대신 이 문자열을 암호화해야 합니다. 아래는 Web.config에 저장되고 BestLogin.aspx에서 사용되는 암호화된 연결 문자열의 모습입니다.

<add key="cnxNWindBest"
value="AQAAANCMnd8BFdERjHoAwE/
Cl+sBAAAAcWMZ8XhPz0O8jHcS1539LAQAAAACAAAAAAADZgAAqAAAABAAAABdodw0YhWfcC6+
UjUUOiMwAAAAAASAAACgAAAAEAAAALPzjTRnAPt7/W8v38ikHL5IAAAAzctRyEcHxWkzxeqbq/
V9ogaSqS4UxvKC9zmrXUoJ9mwrNZ/
XZ9LgbfcDXIIAXm2DLRCGRHMtrZrp9yledz0n9kgP3b3s+
X8wFAAAANmLu0UfOJdTc4WjlQQgmZElY7Z8"
/>

BestLogin은 그림 9에 나와 있는 것과 같이 SecureConnection 클래스로부터 GetCnxString 메서드를 호출하여 cnxNWindBest AppSetting 값을 가져와 다음과 같은 코드로 그 암호를 해독합니다.

 string strCnx = SecureConnection.GetCnxString("cnxNWindBest");

그러면 이번에는 SecureConnection 클래스가 DataProtect 클래스 라이브러리(본 문서에는 나타나 있지 않으나 본 문서를 위한 다운로드에 포함되어 있음)를 호출하는데 이것이 Win32 DPAPI(Data Protection API)에 대한 호출을 래핑합니다. DPAPI의 우수한 기능 중 하나는 여러분을 위해 암호화 키를 관리한다는 점입니다. 사용 시 고려해야 할 추가 옵션을 비롯한 DataProtect 클래스 라이브러리에 대한 자세한 내용은 Microsoft 패턴 및 관행(patterns and practices) 가이드 "안전한 ASP.NET 응용 프로그램 구축: 인증, 권한 부여 및 보안 통신" 을 참조하십시오.

Figure 10 EncryptCnxString.aspx
그림 10  EncryptCnxString.aspx

EncryptCnxString.aspx 페이지를 사용하면 시스템별로 암호화된 연결 문자열을 생성하여 구성 파일에 붙여넣을 수 있습니다.이 페이지는 그림 10에 나와 있습니다.물론, 암호 및 연결 문자열 외에도 신용 카드 번호를 비롯하여 해커에게 노출되었을 때 해가 될 수 있는 기타 정보 등 암호화 또는 해시가 필요한 다른 기밀 정보도 있습니다. ASP.NET 2.0에는 암호 해시와 연결 문자열의 암호화를 단순화하는 많은 기능이 들어 있습니다.


능숙한 오류 처리

런타임 예외를 미숙하게 처리하는 것도 해커들이 사용하고자 하는 또 다른 취약점입니다. 따라서 모든 프로덕션 코드에 예외 처리기를 포함시키는 것이 중요합니다. 또한 처리된 예외와 처리되지 않은 예외 모두 해커에게 도움이 될만한 정보 제공을 항상 최소화해야 합니다. 처리된 예외의 경우, 오류 메시지를 작성할 때 정직한 사용자에게 도움이 되는 선을 넘어서 부도덕한 해커에게 너무 많은 정보를 제공하지 않도록 균형을 유지해야 합니다.

처리되지 않은 예외의 경우, 컴파일 요소(Web.config 파일에서)의 디버그 특성을 false로 설정하고 customErrors 요소의 모드 특성을 On 또는 RemoteOnly로 설정하여 해커에게 도움이 될만한 부분을 최소한으로 유지해야 합니다. 예를 들어 아래를 참조하십시오.

<compilation defaultLanguage="c#"
    debug="false"
/>

<customErrors mode="RemoteOnly" 
/>

RemoteOnly 설정은 로컬 호스트에서 해당 사이트에 액세스하는 사용자에게는 일정한 정보를 담은 오류 메시지를 제공하되 원격 위치에서 해당 사이트에 액세스하는 사용자들에게는 예외에 대한 어떠한 유용한 정보도 노출하지 않는 일반 오류 메시지를 제공하도록 보장합니다. On 설정을 사용하면 로컬 사용자를 포함한 모든 사용자가 일반 오류 메시지를 받게 됩니다. 프로덕션 환경에서는 절대 Off 설정을 사용하지 마십시오.


결론

SQL 주입 공격은 안전하다고 생각되는 시스템에 침입하여 데이터를 절도, 변경 또는 파괴하는 행위에 사용될 수 있으므로 응용 프로그램 개발자에게는 심각한 문제가 됩니다. 어떤 ASP.NET 버전을 사용하든 이러한 공격에 노출되기는 너무나 쉽습니다. 사실은 ASP.NET을 사용하지 않아도 SQL 주입 공격에 노출될 수 있습니다. Windows Forms 응용 프로그램을 비롯하여 사용자가 입력하는 데이터를 사용하여 데이터베이스를 쿼리하는 모든 응용 프로그램은 주입 공격의 대상이 될 수 있습니다.

SQL 주입 공격으로부터 시스템을 보호하는 것은 그리 어렵지 않습니다. 모든 사용자 입력을 확인하여 문제 있는 부분을 제거하고, 동적 SQL을 절대 사용하지 않으며, 최소 권한의 계정을 사용하여 실행하고, 기밀 정보를 해시 또는 암호화하며, 해커에게 유용한 정보가 포함되지 않도록 오류 메시지를 작성하는 응용 프로그램은 SQL 주입 공격에 대한 저항력이 있습니다. 다층적인 보호 방식을 따른다면 방어 수단 하나가 무너지더라도 시스템이 안전하게 보호될 수 있습니다. [주입 공격에 대한 취약점이 있는지 응용 프로그램을 테스트하는 것에 대한 정보는 사이드바의 "주입 공격 테스트 "를 참조하십시오.]



Paul Litwin 은 시애틀의 Fred Hutchinson Cancer Research Center에 근무하는 수석 프로그래머입니다. 그는 Microsoft ASP.NET Connections 컨퍼런스의 회원으로 활동하고 있으며 .NET 교육 회사인 Deep Training을 소유하고 있습니다. Paul에 대한 자세한 정보는 http://www.deeptraining.com 을 참조하십시오..

출처: MSDN Magazine2004년호
가까운 신문판매점을 이용하시거나 구독 신청하십시오.

http://www.microsoft.com/Korea/MSDN/MSDNMAG/ISSUES/2004/SQLInjection/default.aspx