[php] mysql_real_escape_string ()을 둘러싼 SQL 주입

mysql_real_escape_string()함수를 사용할 때에도 SQL 삽입 가능성이 있습니까?

이 샘플 상황을 고려하십시오. SQL은 다음과 같이 PHP로 구성됩니다.

$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));

$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";

나는 많은 사람들이 그런 코드가 여전히 위험하고 mysql_real_escape_string()사용 된 기능으로 도 해킹 할 수 있다고 말합니다 . 그러나 가능한 악용을 생각할 수 없습니까?

다음과 같은 클래식 주사 :

aaa' OR 1=1 --

작동하지 않습니다.

위의 PHP 코드를 통해 얻을 수있는 주입에 대해 알고 있습니까?



답변

다음 쿼리를 고려하십시오.

$iId = mysql_real_escape_string("1 OR 1=1");    
$sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string()이것으로부터 당신을 보호하지 않습니다.
쿼리 내 변수 주위에 작은 따옴표 ( ' ') 를 사용한다는 사실 이이를 방지합니다. 다음도 옵션입니다.

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";


답변

짧은 대답은 그렇습니다. 그렇습니다mysql_real_escape_string() .

매우 엣지 케이스의 경우 !!!

긴 대답은 쉽지 않습니다. 여기서 시연 된 공격을 기반으로합니다 .

공격

이제 공격을 보여 주면서 시작하겠습니다.

mysql_query('SET NAMES gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

특정 상황에서는 둘 이상의 행이 반환됩니다. 여기서 무슨 일이 일어나고 있는지 해부하자 :

  1. 문자 세트 선택

    mysql_query('SET NAMES gbk');

    작업이 공격을 위해, 우리는 서버의 두 인코딩에 대한 연결을 기대하는 인코딩을 필요 'ASCII의 예에서와 같이 0x27 하고 , 그 최종 바이트는 ASCII 일부 문자를 가지고 \0x5c. 그것이 나오는 것에 따라, 기본적으로 MySQL의 5.6에서 지원 (5)와 같은 인코딩이있다 : big5, cp932, gb2312, gbksjis. gbk여기서 선택 하겠습니다.

    자, SET NAMES여기서 의 사용에 주목하는 것이 매우 중요합니다 . 이것은 서버에 문자 세트를 설정합니다 . C API 함수 호출을 사용했다면 mysql_set_charset()(2006 년 이후 MySQL 릴리스에서) 괜찮을 것이다. 그러나 왜 잠시 후에 더 많은 …

  2. 페이로드

    이 주입에 사용할 페이로드는 바이트 시퀀스로 시작합니다 0xbf27. 에서 gbk유효하지 않은 멀티 바이트 문자입니다. 에서 latin1문자열 ¿'입니다. 에 그주의 latin1 하고 gbk , 0x27자신이 리터럴에 '문자.

    우리는이 페이로드를 선택했다. 왜냐하면 호출 하면 문자 앞에 addslashes()ASCII \즉을 삽입 0x5c하기 때문 '이다. 우리가 바람 것 그래서 0xbf5c27, 어떤에서이 gbk두 개의 문자 순서입니다 : 0xbf5c다음에 0x27. 즉, 유효한 문자 뒤에 이스케이프 처리되지 않은이 '있습니다. 그러나 우리는을 사용하지 않습니다 addslashes(). 다음 단계로 넘어갑니다 …

  3. mysql_real_escape_string ()

    C API 호출 은 연결 문자 집합을 알고 있다는 mysql_real_escape_string()점과 다릅니다 addslashes(). 따라서 서버가 기대하는 문자 세트에 대해 이스케이프를 올바르게 수행 할 수 있습니다. 그러나 지금까지 클라이언트 latin1는 연결에 대해 계속 사용하고 있다고 생각합니다 . 우리는 서버에 사용하고 있다고 말 gbk했지만 클라이언트는 여전히 서버 를 사용 한다고 생각합니다 latin1.

    따라서 mysql_real_escape_string()백 슬래시 를 삽입 하라는 호출은 '“탈출 된”컨텐츠에 자유롭게 걸려있는 문자를 갖습니다 ! 우리가보고 있다면 사실, $var에서 gbk문자 집합, 우리는 볼 것 :

    縗 'OR 1 = 1 / *

    이는 정확히 공격이 필요합니다.

  4. 쿼리

    이 부분은 형식 일 뿐이지 만 렌더링 된 쿼리는 다음과 같습니다.

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1

축하합니다, 당신은 성공적으로 mysql_real_escape_string()

나쁜

악화된다. PDO기본적으로 MySQL 을 사용 하여 준비된 명령문 을 에뮬레이트 합니다. 즉, 클라이언트 측에서는 기본적으로 mysql_real_escape_string()(C 라이브러리에서) 스프린트를 수행하므로 다음과 같이 성공적으로 주입됩니다.

$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

이제 에뮬레이트 된 준비된 문장을 비활성화하여 이것을 막을 수 있다는 점에 주목할 가치가 있습니다.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

이로 인해 일반적으로 준비된 명령문이 생성됩니다 (예 : 데이터가 쿼리와 별도의 패킷으로 전송 됨). 그러나 PDO는 MySQL이 기본적으로 준비 할 수없는 명령문 ( 매뉴얼에 나열 될 수 있지만 적절한 서버 버전을 선택하도록주의 해야하는 명령문)을 에뮬레이션하는 것으로 자동으로 대체 됩니다 .

못난이

나는 처음에 우리가 mysql_set_charset('gbk')대신에 사용했다면이 모든 것을 막을 수 있다고 말했다 SET NAMES gbk. 2006 년 이후로 MySQL 릴리스를 사용하는 경우에도 마찬가지입니다.

당신은 이전 MySQL의 릴리스, 다음 사용하는 경우 버그 에서 mysql_real_escape_string()같은 우리의 페이로드에있는 것과 같은 잘못된 멀티 바이트 문자를 목적으로 탈출을위한 단일 바이트로 처리 된 것을 의미 클라이언트가 올바르게 연결 인코딩을 통보했다하더라도 그래서이 공격 것 등을 여전히 성공합니다. 버그는 MySQL 4.1.20 , 5.0.225.1.11 에서 수정되었습니다 .

그러나 최악의 부분은 5.3.6까지 PDOC API를 공개하지 않았기 mysql_set_charset()때문에 이전 버전에서는 가능한 모든 명령에 대해이 공격을 막을 수는 없습니다 ! 이제 DSN 매개 변수 로 노출됩니다 .

구원의 은총

처음에 말했듯이이 공격이 작동하려면 데이터베이스 연결이 취약한 문자 세트를 사용하여 인코딩되어야합니다. utf8mb4이다 취약하지 지원할 수 아직하고 모든 당신의 MySQL 5.5.3 이후 대신-하지만 그것은 단지되었습니다 사용할 수를 사용하여 선택할 수 있도록 유니 코드 문자를. 대안은 utf8또한, 이는 취약하지 및 유니 코드의 전체 지원할 수있는 기본 다국어 평면 .

또는 NO_BACKSLASH_ESCAPESSQL 모드를 활성화 할 수 있습니다.이 모드는 다른 작업 중에서의 작업을 변경합니다 mysql_real_escape_string(). 이 모드가 활성화 된 상태 0x27로 대체됩니다 0x2727보다는 0x5c27탈출 과정이 이렇게하고 할 수없는 그들이 이전에 존재하지 않았던 취약한 인코딩의 유효 문자를 생성 (즉 0xbf27여전히 0xbf27등.) – 서버, 그래서 여전히 유효로 문자열을 거부합니다 . 그러나이 SQL 모드를 사용하여 발생할 수있는 다른 취약점에 대해서는 @eggyal의 답변 을 참조하십시오 .

안전한 예

다음 예는 안전합니다.

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

서버가 기대하기 때문에 utf8

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

클라이언트와 서버가 일치하도록 문자 세트를 올바르게 설정했기 때문입니다.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

우리는 에뮬레이트 된 준비된 진술을 해제했기 때문에.

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

문자 세트를 올바르게 설정했기 때문입니다.

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

MySQLi는 항상 진실 된 준비된 진술을하기 때문입니다.

마무리

만약 너라면:

  • 최신 버전의 MySQL (최근 5.1, 모든 5.5, 5.6 등) mysql_set_charset() / $mysqli->set_charset()/ PDO의 DSN 문자 세트 매개 변수 사용 (PHP ≥ 5.3.6)

또는

  • 연결 인코딩에 취약한 문자 세트를 사용하지 마십시오 ( utf8/ latin1/ ascii/ 등 만 사용 )

당신은 100 % 안전합니다.

그렇지 않으면, 당신 이 사용하더라도mysql_real_escape_string() 취약합니다 …


답변

TL; DR

mysql_real_escape_string()다음 과 같은 경우 보호 기능제공하지 않으며 (또한 데이터를 조정할 수 있습니다)

  • MySQL의의 NO_BACKSLASH_ESCAPESSQL 모드가 활성화되어 (이되는 수도 수, 당신은하지 않는 명시 적으로 다른 SQL 모드를 선택 하면 연결할 때마다 ); 과

  • SQL 문자열 리터럴은 큰 따옴표 "문자를 사용하여 인용 됩니다.

이것은 버그 # 72458 로 제출 되었으며 MySQL v5.7.6에서 수정되었습니다 ( 아래 ” 저축 유예 ” 섹션 참조).

이것은 또 다른 (아마도 덜?) 모호한 EDGE CASE입니다!

에 경의에서 @ ircmaxell 우수한 대답 (정말, 이것은 아첨하지 표절 있어야하는데!), 나는 그의 형식을 채택한다 :

공격

데모로 시작하는 중 …

mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); // could already be set
$var = mysql_real_escape_string('" OR 1=1 -- ');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

test테이블 에서 모든 레코드를 반환 합니다. 해부 :

  1. SQL 모드 선택

    mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');

    문자열 리터럴에 설명 된대로 :

    문자열 내에 인용 부호를 포함시키는 방법에는 여러 가지가 있습니다.

    • '“로 인용 된 문자열 내부의 ” '“는 ” “로 쓸 수 있습니다 ''.

    • "“로 인용 된 문자열 내부의 ” "“는 ” “로 쓸 수 있습니다 "".

    • 따옴표 문자는 이스케이프 문자 (“ \”)로 시작합니다.

    • '“로 인용 된 문자열 내부의 ” "“는 특별한 처리가 필요하지 않으며 배가되거나 이스케이프 될 필요가 없습니다. 같은 방식으로 ” "“로 인용 된 문자열 내부의 ” '“는 특별한 처리가 필요하지 않습니다.

    서버의 SQL 모드에가 포함 된 NO_BACKSLASH_ESCAPES경우 이러한 옵션 중 세 번째 옵션 (일반적으로 채택 된 접근 방식)을 mysql_real_escape_string()사용할 수 없습니다. 대신 처음 두 옵션 중 하나를 사용해야합니다. 네 번째 글 머리 기호의 효과는 데이터를 녹이지 않기 위해 리터럴을 인용하는 데 사용될 문자를 반드시 알아야한다는 것입니다.

  2. 페이로드

    " OR 1=1 -- 

    페이로드는 "캐릭터 와 함께 문자 그대로이 주입을 시작합니다 . 특별한 인코딩이 없습니다. 특수 문자가 없습니다. 이상한 바이트가 없습니다.

  3. mysql_real_escape_string ()

    $var = mysql_real_escape_string('" OR 1=1 -- ');

    다행히 mysql_real_escape_string()SQL 모드를 확인하고 그에 따라 동작을 조정합니다. 참조 libmysql.c:

    ulong STDCALL
    mysql_real_escape_string(MYSQL *mysql, char *to,const char *from,
                 ulong length)
    {
      if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES)
        return escape_quotes_for_mysql(mysql->charset, to, 0, from, length);
      return escape_string_for_mysql(mysql->charset, to, 0, from, length);
    }

    따라서 SQL 모드가 사용중인 escape_quotes_for_mysql()경우 다른 기본 함수 인을 호출합니다 NO_BACKSLASH_ESCAPES. 위에서 언급 한 바와 같이, 그러한 함수는 다른 인용 문자가 문자 그대로 반복되지 않고 문자를 반복하기 위해 문자를 인용하는 데 사용될 문자를 알아야합니다.

    그러나이 함수는 임의로 작은 따옴표 문자를 사용하여 문자열을 인용 한다고 가정 합니다 '. 참조 charset.c:

    /*
      Escape apostrophes by doubling them up
    
    // [ deletia 839-845 ]
    
      DESCRIPTION
        This escapes the contents of a string by doubling up any apostrophes that
        it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in
        effect on the server.
    
    // [ deletia 852-858 ]
    */
    
    size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info,
                                   char *to, size_t to_length,
                                   const char *from, size_t length)
    {
    // [ deletia 865-892 ]
    
        if (*from == '\'')
        {
          if (to + 2 > to_end)
          {
            overflow= TRUE;
            break;
          }
          *to++= '\'';
          *to++= '\'';
        }

    따라서 리터럴을 인용하는 데 사용되는 실제 문자에 관계없이 큰 따옴표 "문자를 그대로 유지하고 모든 작은 따옴표 '문자를 두 배로 만듭니다 ! 우리의 경우에 $var제공 한 인수와 정확히 동일하게 유지 mysql_real_escape_string()에는 이스케이프가 자리를 차지하지 않았다 것처럼 – 그것은이다 전혀 .

  4. 쿼리

    mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

    형식적인 것으로 렌더링 된 쿼리는 다음과 같습니다.

    SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1

배운 친구가 말했듯이 축하합니다. 방금 mysql_real_escape_string()

나쁜

mysql_set_charset()이것은 문자 세트와 관련이 없으므로 도움을 줄 수 없습니다. mysqli::real_escape_string()이 함수를 둘러싼 다른 래퍼 일 뿐이므로 수 없습니다 .

문제는 아직 명확하지 않은 경우 문자를 인용 할 문자를 mysql_real_escape_string() 알 수 없다는 요청 은 개발자가 나중에 결정해야한다는 것입니다. 따라서 NO_BACKSLASH_ESCAPES모드에서는 이 함수가 임의의 인용 부호와 함께 사용하기 위해 모든 입력을 안전하게 이스케이프 할 수있는 방법 이 없습니다.

못난이

악화된다. NO_BACKSLASH_ESCAPES표준 SQL과의 호환성에 대한 사용의 필요성 때문에 야생에서 드문 일이 아닐 수도 있습니다 (예 : SQL-92 사양 의 섹션 5.3 , 즉 <quote symbol> ::= <quote><quote>문법 생성 및 백 슬래시에 주어진 특별한 의미가 없음). 또한 ircmaxell의 게시물에서 설명 하는 (고정 된 이후의) 버그에 대한 해결 방법 으로 사용하는 것이 명시 적으로 권장되었습니다 . 누가 알 수 있듯이 일부 DBA는와 같은 잘못된 이스케이프 메소드 사용을 권장하지 않기 위해 기본적으로 사용하도록 구성 할 수도 있습니다 .addslashes()

또한 새 연결SQL 모드 는 구성에 따라 서버에서 설정합니다 ( SUPER사용자는 언제든지 변경할 수 있음). 따라서 서버의 동작을 확실하게하려면 연결 후 항상 원하는 모드를 명시 적으로 지정해야합니다.

구원의 은총

작은 따옴표 문자를 사용하여 MySQL 문자열 리터럴 을 포함 하거나 인용 하지 않도록 SQL 모드 를 항상 명시 적으로 설정하는 NO_BACKSLASH_ESCAPES한이 버그는 추한 머리를 깎을 수 없습니다. 각각 escape_quotes_for_mysql()사용되지 않거나 따옴표 문자를 반복 해야하는 가정 정확하다.

이러한 이유로 작은 인용 문자열 리터럴을 습관적으로 사용하므로 모드를 사용하는 사람 NO_BACKSLASH_ESCAPESANSI_QUOTES모드를 사용하는 것이 좋습니다 . 큰 따옴표로 묶인 리터럴이 사용되는 경우 SQL 삽입을 막을 수는 없습니다. 이는 정상적인 악의없는 쿼리가 실패하기 때문에 발생 가능성을 줄입니다.

PDO에서 동등한 기능 PDO::quote()과 준비된 명령문 에뮬레이터 mysql_handle_quoter()는 다음과 같이 요구합니다. 정확하게이 작업을 수행합니다. 이스케이프 된 리터럴이 작은 따옴표로 인용되므로 PDO가 항상이 버그로부터 영향을받지 않습니다.

MySQL v5.7.6부터이 버그가 수정되었습니다. 변경 로그를 참조하십시오 .

추가되거나 변경된 기능

안전한 예

ircmaxell에 의해 설명 된 버그와 함께, 다음 예제는 완전히 안전합니다 (4.1.20, 5.0.22, 5.1.11 이후의 MySQL을 사용하거나 GBK / Big5 연결 인코딩을 사용하지 않는 것으로 가정). :

mysql_set_charset($charset);
mysql_query("SET SQL_MODE=''");
$var = mysql_real_escape_string('" OR 1=1 /*');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

… 포함되지 않은 SQL 모드를 명시 적으로 선택했기 때문 NO_BACKSLASH_ESCAPES입니다.

mysql_set_charset($charset);
$var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

… 문자열 리터럴을 작은 따옴표로 인용하기 때문입니다.

$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);

… PDO 준비된 명령문은이 취약점으로부터 영향을받지 않기 때문에 (PHP≥5.3.6을 사용하고 문자 세트가 DSN에서 올바르게 설정되었거나 준비된 명령문 에뮬레이션이 비활성화 된 경우 ircmaxell도 마찬가지 임) .

$var  = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");

… PDO의 quote()함수는 리터럴을 이스케이프 할뿐만 아니라 작은 따옴표 '문자로 도 인용하기 때문에 ; 이 경우 ircmaxell의 버그를 방지 할 수 있습니다, 당신은 해야한다 PHP≥5.3.6를 사용 하고 올바르게 DSN의 문자 집합을 설정했습니다.

$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

… MySQLi 준비 문장이 안전하기 때문에.

마무리

따라서 다음과 같은 경우 :

  • 기본 준비된 진술을 사용하십시오

또는

  • MySQL v5.7.6 이상 사용

또는

  • 에서 또한 적어도 하나의 ircmaxell의 요약에있는 솔루션 중 하나, 사용을 사용하기 :

    • PDO;
    • 작은 따옴표로 묶인 문자열 리터럴; 또는
    • 포함되지 않은 명시 적으로 설정된 SQL 모드 NO_BACKSLASH_ESCAPES

… 그런 다음 완전히 안전 해야 합니다 (문자열 이탈 범위를 벗어난 취약점).


답변

글쎄, %와일드 카드를 제외하고는 실제로 그것을 통과 할 수있는 것은 없습니다 . LIKE침입자가 %이를 걸러 내지 않으면 로그인 과 같은 방식으로 문장을 사용하는 경우 위험 할 수 있으며 사용자의 암호를 무차별 처리해야합니다. 사람들은 종종 준비된 명령문을 사용하여 데이터를 쿼리 자체를 방해하지 않기 때문에 100 % 안전하도록 제안합니다. 그러나 이러한 간단한 쿼리의 경우 다음과 같은 작업을 수행하는 것이 더 효율적일 것입니다$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);


답변