관리 메뉴

엉망진창

보안강좌 : SQL 인젝션(Injection) 공격을 막자! - Taeyo's ASP.NET 출처 본문

Study_DB/DB_MSSQL

보안강좌 : SQL 인젝션(Injection) 공격을 막자! - Taeyo's ASP.NET 출처

엉망진창 2008. 3. 21. 10:34


Taeyo's ASP.NET

   강좌 최초 작성일 : 2003년 08월 02일
   강좌 최종 수정일 : 2003년 08월 04일

   강좌 읽음 수 : 27211 회

   작성자 : Taeyo(김 태영)
   편집자 : Taeyo(김 태영)

   강좌 제목 : 보안강좌! SQL Injection!!

강좌 전 태오의 잡담>

^^; 태오 쥬니어가 이제 곧 출시를 앞두고 있습니다. 여러가지 트러블이 있지만, 안전성을 유지하면서 버그없이 출시되기를 바라는 마음이 간절합니다. 홧팅.. 쥬냐~~


안녕하세요? 태오입니다.

이번 강좌에서는 그동안 수도 없이 강조했지만 여전히 지켜지지 않고 있는 나쁜 프로그래밍 습관에 대한 이야기를 해볼까 합니다. 그러한, 나쁜(?) 프로그래밍 코드가 사이트를 보안적으로 위험하게 만들고 있고, 그를 통해서 여러 형태의 해킹이 시도되고 있는 형편이기에 제 사이트에서도 강력하게 이 내용을 명시적으로(!) 다뤄줘야 한다는 요청(?)이 있어서요 -_-+

그 내용은 다름이 아닌 SQL 인젝션에 대한 이야기입니다.

물론, 현재 많이들 공격받고 있는 방법에는 SQL 인젝션만 있는 것은 아닙니다. 크로스 사이트 스크립팅(XSS), SQL인젝션 등이 게시판을 통해서 공격이 가능한 해킹 방법들인데요. 위험하기로 따지면 XSS 보다는 SQL 인젝션이 더더욱 위험하기에 이 내용부터 우선적으로 다루려고 합니다.

이 인젝션 공격은 최근에 모 결혼정보 사이트에서도 호되게 당한적이 있는 공격이기도 하지요. 내용이 궁금하신 분은 다음 기사를 한번 읽어보시기 바랍니다.

네이버 뉴스 : [구멍난 e코리아] 보안시스템의 한계

특히나, 보안은 최근 마이크로소프트의 최대 이슈사항이기에 더욱 이 강좌에 힘을 실어야 하지 않나 싶습니다. 호곡... 갑자기 힘을 주어야 한다고 하니... 갑자기... 엇... Poooooozic!! -_-;

그렇다면, SQL 인젝션이란 것은 무엇이냐???

자세한 내용은 사실, 제가 2004년에 Microsoft Developer days 2004에서 발표했던 Developing Secure Web Applications 자료(보고 싶다면 여길 클릭!!)를 보시면 도움이 되시겠지만 말입니다.

이는 SQL 쿼리 문자열 사이에 음흉한(?) 코드를 끼어넣어서 실행시키게 하는 공격 방법을 말합니다. 이러한 공격이 가능하려면 다음과 같은 필요조건이 요구되지요.

1. 사용자의 입력 값을 필터링, 인코딩 없이 그대로 서버 페이지에서 사용한다.
2. 그 입력 값을 이용해서 서버 페이지는 쿼리를 수행한다.
2. 명령 실행을 위해 사용되는 SQL 서버 계정이 SA일 경우, 피해는 더 커질 수 있다.

이러한 조건을 확실하게 방어한다면 사실 SQL 인젝션은 그다지 두려운 공격이 아닙니다. 미연에 이러한 공격을 못하도록 보안적으로 코드를 구현한다면 말입니다.

그런데!!!

문제는 수 많은 사이트가 위의 조건을 만족시키는 형태의 코드를 사용하고 있다는 것입니다. 어떤 경우는 개발자의 무지로 인해서, 어떤 경우는 알면서도 바꾸기 귀찮아서 등의 이유로 인해서 말입니다.

그렇다면, 한번 해 봅시다. SQL 인젝션의 대표적인 희생양이 될 수 있는 경우를 말입니다. 일단, 여러분이 사이트 개발자고 여러분의 사이트에서 어떤 검색 기능이라던가 글 저장기능이라던가, 회원 가입 기능이라던가... 어떤 기능이던지 간에 사용자로부터 입력 받은 값을 데이터베이스에 반영하는 모듈이 있다고 해봅시다. 저의 경우는 사용자로부터 사용자 아이디를 입력받아서 그러한 아이디가 데이터베이스에 존재하는 경우에는 YES를, 존재하지 않는 경우에는 NO를 출력하는 단순한 페이지를 만들어 보았습니다. 하지만, 이러한 비슷한 유형의 코드를 여러분도 사용하고 있을 것이며, 그렇기에 코드를 이러한 형태로 작성했다는 것은 현재 상당히 위험하다는 것을 의미합니다.

어떤 점이 문제인 거냐구요??  어떤 부분이 위험한 거냐구요? 그것은 코드를 먼저 보고 설명드리도록 하겠습니다.

일단, 웹 폼에는 입력을 받기 위한 TextBox 하나와 메시지 출력을 위한 Label 컨트롤 하나, 그리고 Button 컨트롤이 하나 올라가 있습니다요. 사용자는 TextBox에 검색을 위한 아이디를 입력하고, 버튼을 클릭합니다. 그러면, 그 경우 다음과 같은 코드가 동작하게 되는 것이지요.

private void Button1_Click(object sender, System.EventArgs e)
{
    string id = Id.Text;
    string Status = "No";
    string sqlString="";

    SqlConnection con = new SqlConnection("server=(local);database=Test;uid=sa;pwd=**");

    try
    {
        con.Open();

        sqlString = string.Format("SELECT name FROM Client WHERE ID='{0}'", id);
        Response.Write(sqlString + "<BR>");

        SqlCommand cmd = new SqlCommand(sqlString, con);
        if(cmd.ExecuteScalar() != null)
            Status = "YES";
    }
    catch(SqlException ex)
    {
        Status = "Failed\n";
        foreach(SqlError er in ex.Errors)
            Status += er.Message + "\n";
        }
    catch(Exception exp)
    {
        Status = exp.ToString();
    }
    finally
    {
        con.Close();
        con = null;
    }

    Label1.Text = Status;
}

이 코드의 문제점은 크게 3개 입니다.

1. 데이터베이스 연결을 sa 계정을 이용해서 하고 있습니다.
2. 사용자의 입력값을 어떠한 검사도 없이 입력 들어온 그대로 사용하고 있습니다.
3. SQL 쿼리 문자열을 문자열 결합으로 만들어 그대로 데이터베이스로 날려 명령을 수행하고 있습니다.

이게 왜 엄청난 문제들인지를 지금부터 설명하도록 하겠습니다(이에 대한 이유를 아시는 분들은 내공이 있으신 분들이니 ^^;;  이번 강좌는 살포시 웃으면서 지켜봐주셔도 좋겠습니다. 히히히~)

그럼 실습과 함께 충격을 피부로 느껴보는 시간을 갖도록 하겠습니다. 그래야 기억도 오래가고 뭔가 아픔도 클 것 같네염~~~  히히.. 아픈만큼 성숙해진다구 하니까요~ -_-;;;;;;

만일, 사용자가 정상적으로 다음과 같이 입력한다고 가정해 보아요~~ 물론, 대부분의 사용자들이 이렇게 착하답니다. ^^;

아이디가 1001인 사용자가 있을 경우, 그 아이디를 입력하고 버튼을 눌러 결과를 실행시키면 다음과 같이 정상적으로 처리가 되는 것을 볼 수 있습니다.

근데, 말입니다. 이 세상 모든 사람들이 이렇게 착한 것이 아니라는 것이 문제죠. 어떤 이는 별로 안 좋은 행동을 하기도 한다는 겁니다. 예를 들면, 다음과 같이 한번 입력해 보기도 한다는 것이죠.

허걱. 이것은 무서운 명령이 아닐 수 없습니다. 그렇죠? 보기만 해도 끔찍하죠???

-_-;;; 잘 모르시겠다구요???

사용자가 1001' ; Shutdown --와 같이 입력하게 되고, 이 명령을 쿼리 문자열에 직접 사용하게 되면 데이터베이스로의 명령은 화면에서 보이다시피 다음과 같이 꾸며지게 됩니다.

SELECT name FROM Client WHERE ID='1001' ; Shutdown --'

이 쿼리는 재미있게도 완전하게 구성된 총 2개의 구문으로 SQL 서버는 인식하게 되는데요. 즉,

SELECT name FROM Client WHERE ID='1001'
Shutdown --'

와 같이 2 줄로써 수행되는 것과 같은 결과를 만들어내게 됩니다. 그리고, 이 결과는 참혹하게도 데이터베이스 서버를 죽이는 결과를 낳게 되죠. Shutdown 이라는 명령이 바로 데이터베이스 서비스를 멈추는 명령이니까요 ^^(참고 : -- 는 SQL 서버에서 주석을 의미하기에 -- 뒤의 구문들은 모두 무시됩니다)  호곡!!! 참혹하지 않나요??

만일, "에게~~~ 이 정도 가지구 뭘...  서버야 죽으면 다시 살리면 되잖아.. 뭘" 이라고 말도 안되게 배짱이 좋으시다면 다음과 같은 명령은 어떻습니까??

exec xp_cmdshell 'format d:'

그렇습니다. 일케 하면 서버의 D 드라이브는 홀라당 포맷되어져 날아갑니다. 오오오~~~  이젠 조금 몸서리가 쳐지시나요? ㅠㅠ ; 보안이란 게 말입니다. 몸서리가 쳐질 때 쯤이면 이미 늦은 것입니다. ㅜㅜ

자. 이러한 문제가 생기게 된 이유를 하나씩 살펴보도록 하겠습니다

우선!! 가장 큰 문제는 Shutdown이나 exec xp_cmdshell 와 같은 끔찍한 명령이 어떻게 실행될 수 있느냐는 겁니다. 이런 명령은 관리자급의 계정이 아니면 실행할 수 없는 명령인데 말입니다. 그것은 아직도 많은 분들이 데이터베이스 연결 문자열에 User id로 sa를 사용하고 있기에 그렇습니다. 그렇게 되면, 데이터베이스 연결 시 계정이 초 울트라 슈퍼 사이어 계정인 sa로 설정되기에, SQL 서버에 대해 모든 명령을 사용할 수가 있게 됩니다. 이것은 정말로 큰 문제가 아닐 수 없는 것이죠!!!!!!!  그래서, 기존 세미나나 강좌들에서 "제발 좀!!! 제발 쫌~~~ sa 계정을 사용하지 말아주세요.." 라고들 외쳐왔던 것이옵니다~~~~

이것만 제한적인 계정으로 바꾸어줘도...  보안적으로는 상당히 안전해 집니다. 적어도, 해커가 시스템까지 가해할 수는 없게 될테니까요. 그래서, 첫번째 지켜야 하는 규칙!

1. 데이터베이스 연결에는 가장 제한적인 계정을 사용한다!!!

입니다. 예를 들자면, 여러분이 현재 사용하는 데이터베이스가 MyDB라면, 특정 계정(예를 들면, MyUser)라는 계정을 만들어서 그 계정만이 MyDB에 접근할 수 있도록 제한한다는 것이죠. 그러면, 아주 빼~~~드한 사용자가 SQL 인젝션을 사용해서 데이터베이스를 다운시키려 해도 그것은 동작하지 않는다는 것입니다. 이 경우는, 데이터베이스 연결 계정이 데이터베이스 서버를 다운시킬 권한이 없기에 Shutdown 명령이 동작하지 않을테니까요~~~

해서, 데이터베이스 연결에 가장 제한적인 계정을 사용하는 것은 무엇보다 우선적으로 지켜줘야 할 규칙입니다. 그렇습니다. 반드시~~~ 꼭~~~~  약속해 주세요 ^^

그리고, 이어지는 두번째 규칙은 사용자가 입력한 값을 최대한 제약한다는 것입니다. 위의 예제에서는 입력값이 사용자의 ID 이지요? 대부분의 ID는 어떤 제약사항같은 것이 있을 것입니다. 예를 들면, 4자리 이상 10자리 이하의 숫자로 구성된 문자열이라던가 하는 등의 제약말이죠..

위의 샘플에서는 사용자 아이디가 숫자로 된 4자리의 문자열로 제약되어져 있습니다. 고로, 이것에 따라 여러분은 사용자의 입력값에 Validation(유효성 검사)를 걸어서 입력을 애초부터 강하게 제약할 필요가 있습니다. 정상적인 사용자라면 이러한 제약에 전혀 불편함을 느끼지 않을 것입니다. 불만을 토로하는 사용자는 모두 빼드~~~한 사용자겠죠~

해서, 규칙 2

2. 사용자의 입력값에 대해서는 가능한 한 최대한 제약을 걸고, 반드시 유효성을 검사한다!!

자. 그 다음에는 쿼리 문자열에 대한 것입니다. 중요성의 무게로 따지면 남바 투가 될 정도로 중요한 규칙이지요. 남바 원은 두말 안해도 "제한적인 계정"이구요~

쿼리 문자열을 코드 상에서 문자열로 결합하여 사용하는 것은 여러모로 대단히 안 좋은 방법입니다. 보안적으로도, 성능적으로도, 효율성 측면에서도, 확장성 측면에서도, 유지보수성 측면에서도, 가독성 측면에서도  안 좋은 방법입니다. 한 마디로!!!  절대로 그렇게 하면 안되는 것이 "쿼리를 코드 상에서 문자열로 결합해서 사용하는 것입니다"

하지만, 사실~~~ 기존에 많은 ASP 서적들이 쿼리를 문자열 결합해서 사용하는 소스를 제공했었죠. 그것은 비단 저도 마찬가지였구요(ㅜㅜ). 그것은.... 보안적으로도 내공적으로도 부족했던 시기에 책이 나왔기에 그렇습니다. 이 부분은 죄송스럽게 생각합니다만, 5,6년 전의 저의 내공은 현재의 이러한 보안적인 문제까지 고려할 수준이 못 되었기에.... ㅠㅠ  그러했던 것이랍니다.

아참!! 만일, 여러분중에 ASP 내공이 상당하신 분들이라면.. 이미 이러한 문제를 알고 계셨던 분들도 있을 것이구요. 그 분들 중에는 쿼리 문자열 결합을 사용하면서도 SQL 인젝션 공격을 피할 수 있는 방법이 있다고 나즈막히 말씀하시는 분들도 있을 것입니다.

굳이 설명을 드리자면, 현재의 SQL 인젝션은 사용자가 입력한 값 중에서 '(작은 따옴표)로 인해 일어나는 것이기에, 사용자가 입력한 값 중 작은 따옴표를 문자 그대로 쿼리에 사용하게끔 만들면 이러한 인젝션을 대충 피해갈 수 있다는 생각을 하고 계실 것이라는 거죠...

예를 들면, 위의 샘플을 다음과 같이 바꾸어서 말입니다.

private void Button1_Click(object sender, System.EventArgs e)
{
    string id = Id.Text;
    string Status = "No";
    string sqlString="";

    SqlConnection con= new SqlConnection("server=(local);database=Test;uid=sa;pwd=**");

    try
    {
        con.Open();

        id = id.Replace("'", "''");

        sqlString = string.Format("SELECT name FROM Client WHERE ID='{0}'", id);
        Response.Write(sqlString + "<BR>");

        SqlCommand cmd = new SqlCommand(sqlString, con);
        if(cmd.ExecuteScalar() != null)
            Status = "YES";
    }
    // .... 중략....

SQL 쿼리문에서 하나의 작은 따옴표를 인식시키기 위해서는 작은 따옴표를 2개연속 붙여서 사용하면 됩니다. 그러면, 사용자가 입력한 값 중 작은 따옴표가 문자 그대로 입력값으로써 사용될 것이기에, SQL 인젝션을 얼추 막을 수 있을 것 처럼 보인다는 것이지요.

소스를 위와같이 바꾸시고, 다시 한번 실행해 보세요. 그러면, 이번에는 다음과 같이 제대로 처리된 결과가 나오는 것을 볼 수 있을 것입니다. 즉, [1001' ; Shutdown --']라는 아이디를 가지고 있는 사용자가 있는지를 검색한다는 것이지요 ^^;;; 그렇게 복잡하고 말도 안되는 아이디를 가진 사용자는 당근 말밥!! 없겠죠?? 그래서, 결과는 No라고 나오는 것을 보실 수 있을 것입니다.

이러한 방법으로 어느정도 SQL 인젝션을 막을 수도 있을 것이긴 합니다. 하지만, 이 방법이 완전한 해결책이 되어줄 수는 없습니다. 소프트웨어란 완벽할 수 없는 노릇이고, 개발자 또한 실수를 하게 마련이니까요. 게다가, 입력 인자가 문자가 아니고 숫자타입인 경우에는 대부분 위와 같은 작은 따옴표처리를 대부분 잊고 지나치곤 하기에, 그러한 경우는 공격의 대상이 될 수 있습니다.

또한, SQL 쿼리를 페이지에서 문자열 결합해서 쓰는 이러한 방법은 BackDoor와 같이 ASP 소스를 볼 수 있는 버그같은 것이 있을 경우에(예전에 종종 이런 문제가 터지곤 했었죠?), 데이터베이스 관련 명령들이 적나라하게 드러난다는 부담도 있습니다. ㅜㅜ

그렇다면, 어쩌라는 것이냐구요????  그렇습니다. 이제 그 이야기를 드려야 할 것 같네요.

해서, 제가 가장 강추하는 것은(그리고, MS 문서에서도 권장하는 것은) 위에서 나열한 모든 측면에서 효과좋고, 엘레강스하고, 완다풀한 저장 프로시저(Stored Procedure)를 사용하는 것입니다. 그리고, 만일, 저장 프로시저를 도저히 사용할만한 입장 및 환경이 못 된다 하시면 그때는 Named 매개변수 방법이라도 사용하시라고 권해드리고 싶습니다.

즉, SQL 쿼리를 위해 사용되는 인자들에 대해서 강력한 형식의 데이터를 사용하라는 것입니다. 저장 프로시저를 사용하게 되면, 각각의 입력 인자에 대해 명확하게 형식을 지정해 주어야만 하죠? 예를 들면, Int, Varchar 등등으로 말입니다. 이러한 지정이 개발자에게는 쓸데없는 추가작업인 것처럼 느껴질런지도 모르겠지만 사실은 이 작업이 바로 보안적으로도, 성능적으로도 대단히 중요한 작업인 것입니다. 사용자가 입력한 값에 대해 형식(Type)적으로도 더욱 치밀한 검사를 수행하게 되는 것이니까요.

고로, 앞으로 저장 프로시저의 인자 형식을 작성할 때는

"나는 지금 대단히 중요한 작업 중이니 건드리지 마셈. 나 건드리면 서버 책임 못 짐"

이라고 자부하면서 작업해주셔도 좋겠단 말씀입니다. 물론, 진짜 그렇게 외칠 경우 뒤따라오는 결과에 대해서는 태오는 아무런 책임이 없습니다. (어느날 점심을 혼자 먹고 있는 자신을 발견하게 될지도... -_-;;;) 개인차라는 게 있으니깐 말입니다.~~ 음핫핫

해서, 3번째 규칙은 다음과 같습니다.

3. 입력값에 대해 강력한 형식을 사용할 수 있는 저장 프로시저를 사용한다.

그렇습니다. 그렇다면, 이러한 규칙(?)을 소스에 반영한 결과를 한번 볼까요?

private void Button1_Click(object sender, System.EventArgs e)
{
    string id = Id.Text;
    string Status = "No";
    string sqlString="";

    SqlConnection con = new SqlConnection("server=(local);database=Test; uid=TestUser; pwd=xoduddlek+@#$");

    try
    {
        con.Open();

        SqlCommand cmd = new SqlCommand("proc_GetName", con);
        cmd.CommandType = CommandType.StoredProcedure;

        cmd.Parameters.Add("@id", SqlDbType.VarChar, 10);
        cmd.Parameters["@id"].Value = id;

        if(cmd.ExecuteScalar() != null)
            Status = "YES";
    }
    catch(SqlException ex)
    {
        Status = "Failed\n";
        foreach(SqlError er in ex.Errors)
            Status += er.Message + "\n";
        }
    catch(Exception exp)
    {
        Status = exp.ToString();
    }
    finally
    {
        con.Close();
        con = null;
    }

    Label1.Text = Status;
> }

소스에서는 기존의 쿼리 문자열 대신 proc_GetName라는 프로시저를 만들어서 사용하고 있는데요. 이 프로시저의 내용은 기존의 쿼리문자열의 것과 동일합니다. 굳이 예를 들자면, 다음과 같다는 것이지요 ^^

CREATE PROC proc_GetName
    @id     varchar(10)
AS
    SELECT [name] FROM Client WHERE ID=@id

그리고, 또한 위의 샘플은 데이터베이스 연결 문자열을 위해서 별도의 TestUser라는 계정을 SQL 서버에 하나 만들어 두고, 그 계정을 사용하고 있습니다. 가장 제한적인 계정을 사용하라는 첫번째 규칙을 따르고 있는 것이지요. 이러한 계정을 만드는 것은 SQL 서버 엔터프라이즈 매니저에서 쉽게 하실 수 있을 것입니다만, [쿼리 분석기]에서도 다음과 같은 방법으로 계정을 추가할 수 있습니다.

USE TEST
GO

-- TEST 데이터베이스에 TestUser라는 이름의 계정을 만든다
EXEC sp_addlogin 'TestUser', 'xoduddlek+@#$', 'TEST'

-- TestUser 계정에게 TEST 데이터베이스에 대한 access를 부여한다
EXEC sp_grantdbaccess 'TestUser'

-- TestUser 계정이 proc_GetName 저장 프로시저를 호출할 수 있게 한다
GRANT EXECUTE ON proc_GetName TO TestUser

그리고 또 한가지 중요한 부분이 있는데요. 사실, 위에서 보시면 데이터베이스 연결 문자열을 소스 코드에서 직접 기입해서 쓰고 있지 않습니까? 사실, 이것은 그다지 바람직한 방법이 아닙니다. 유지 보수성 측면에서도 좋지 않을 뿐만 아니라 보안적으로도 그렇게 소스 코드에 직접 코딩하여 쓰는 것은 결코 바람직하지 않기 때문입니다.

해서, 더 나은 방법으로는 데이터베이스 연결문자열을 로컬 파일 안에 저장시켜 두고, 코드 레벨에서 그 문자열을 동적으로 읽어와서 사용한다던가, 아니면, 레지스트리에 그 연결 문자열을 저장해두고 그것을 동적으로 읽어와서 사용한다던가 하는 방법이 추천됩니다. 비록, 현재의 샘플에서는 그 부분이 빠져있다 하더라도 말입니다. !!!!!

자. 중요한 내용은 이제 어느 정도 정리가 된 것 같네요

그럼 한번 가볍게 정리를 해 볼까요? 다음은 안전한 데이터 접근을 위한 일종의 지침(?)을 약간 정리해 본 것이랍니다

-사용자의 입력은 결코 신뢰하지 않는다
-정규 표현식을 사용하여 적법한 입력 외에는 거부한다.
-쿼리 빌드 시, 문자열 연결이 아닌 매개변수 쿼리를 사용한다. 즉, 저장 프로시저를 사용한다
-Sysadmin 계정이 아닌 최소 권한 계정으로 데이터베이스에 연결하게 한다
-데이터베이스 연결 문자열을 가급적 config 파일에 저장하지 않는다(암호화 권장)
-공격자에게 너무 많은 오류 정보를 노출하지 않는다

많이들 아시는 내용이겠지만, 미처 몰랐던 분들에게는 도움이 되는 강좌였기를 바랍니다. 그럼, 저는 다음 주에 또 새로운 강좌를 들고 찾아오도록 하겠습니다. 날씨가 넘 더운데 모두들 건강하게... 좋은 하루 하루 보내세요.

감사합니다. ^^