Programing

mysql_real_escape_string () 주위를 돌아 다니는 SQL 주입

crosscheck 2020. 10. 3. 09:58
반응형

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 \ie를 삽입 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 라이브러리에서) sprintf를 수행합니다. 즉, 다음과 같은 경우 성공적인 인젝션이 발생합니다.

$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또한 취약하지 않으며 Unicode Basic Multilingual Plane 전체를 지원할 수 있습니다 .

또는 NO_BACKSLASH_ESCAPESSQL 모드를 활성화 할 수 있습니다.이 모드는 무엇보다도 mysql_real_escape_string(). 이 모드를 사용 하면는 대신 0x27로 대체 되므로 이스케이프 프로세스 이전에 존재하지 않았던 취약한 인코딩 (예 : 여전히 존재)에서 유효한 문자를 만들 수 없습니다 . 따라서 서버는 여전히 문자열을 유효하지 않은 것으로 거부합니다. . 그러나이 SQL 모드를 사용할 때 발생할 수있는 다른 취약점에 대해서는 @eggyal의 답변참조하십시오 .0x27270x5c270xbf270xbf27

안전한 예

다음 예는 안전합니다.

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's NO_BACKSLASH_ESCAPES SQL mode is enabled (which it might be, unless you explicitly select another SQL mode every time you connect); and

  • your SQL string literals are quoted using double-quote " characters.

This was filed as bug #72458 and has been fixed in MySQL v5.7.6 (see the section headed "The Saving Grace", below).

This is another, (perhaps less?) obscure EDGE CASE!!!

In homage to @ircmaxell's excellent answer (really, this is supposed to be flattery and not plagiarism!), I will adopt his format:

The Attack

Starting off with a demonstration...

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');

This will return all records from the test table. A dissection:

  1. Selecting an SQL Mode

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

    As documented under String Literals:

    There are several ways to include quote characters within a string:

    • A “'” inside a string quoted with “'” may be written as “''”.

    • A “"” inside a string quoted with “"” may be written as “""”.

    • Precede the quote character by an escape character (“\”).

    • A “'” inside a string quoted with “"” needs no special treatment and need not be doubled or escaped. In the same way, “"” inside a string quoted with “'” needs no special treatment.

    If the server's SQL mode includes NO_BACKSLASH_ESCAPES, then the third of these options—which is the usual approach adopted by mysql_real_escape_string()—is not available: one of the first two options must be used instead. Note that the effect of the fourth bullet is that one must necessarily know the character that will be used to quote the literal in order to avoid munging one's data.

  2. The Payload

    " OR 1=1 -- 
    

    The payload initiates this injection quite literally with the " character. No particular encoding. No special characters. No weird bytes.

  3. mysql_real_escape_string()

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

    Fortunately, mysql_real_escape_string() does check the SQL mode and adjust its behaviour accordingly. See 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);
    }
    

    Thus a different underlying function, escape_quotes_for_mysql(), is invoked if the NO_BACKSLASH_ESCAPES SQL mode is in use. As mentioned above, such a function needs to know which character will be used to quote the literal in order to repeat it without causing the other quotation character from being repeated literally.

    However, this function arbitrarily assumes that the string will be quoted using the single-quote ' character. See 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++= '\'';
        }
    

    So, it leaves double-quote " characters untouched (and doubles all single-quote ' characters) irrespective of the actual character that is used to quote the literal! In our case $var remains exactly the same as the argument that was provided to mysql_real_escape_string()—it's as though no escaping has taken place at all.

  4. The Query

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

    Something of a formality, the rendered query is:

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

As my learned friend put it: congratulations, you just successfully attacked a program using mysql_real_escape_string()...

The Bad

mysql_set_charset() cannot help, as this has nothing to do with character sets; nor can mysqli::real_escape_string(), since that's just a different wrapper around this same function.

The problem, if not already obvious, is that the call to mysql_real_escape_string() cannot know with which character the literal will be quoted, as that's left to the developer to decide at a later time. So, in NO_BACKSLASH_ESCAPES mode, there is literally no way that this function can safely escape every input for use with arbitrary quoting (at least, not without doubling characters that do not require doubling and thus munging your data).

The Ugly

It gets worse. NO_BACKSLASH_ESCAPES may not be all that uncommon in the wild owing to the necessity of its use for compatibility with standard SQL (e.g. see section 5.3 of the SQL-92 specification, namely the <quote symbol> ::= <quote><quote> grammar production and lack of any special meaning given to backslash). Furthermore, its use was explicitly recommended as a workaround to the (long since fixed) bug that ircmaxell's post describes. Who knows, some DBAs might even configure it to be on by default as means of discouraging use of incorrect escaping methods like addslashes().

Also, the SQL mode of a new connection is set by the server according to its configuration (which a SUPER user can change at any time); thus, to be certain of the server's behaviour, you must always explicitly specify your desired mode after connecting.

The Saving Grace

So long as you always explicitly set the SQL mode not to include NO_BACKSLASH_ESCAPES, or quote MySQL string literals using the single-quote character, this bug cannot rear its ugly head: respectively escape_quotes_for_mysql() will not be used, or its assumption about which quote characters require repeating will be correct.

For this reason, I recommend that anyone using NO_BACKSLASH_ESCAPES also enables ANSI_QUOTES mode, as it will force habitual use of single-quoted string literals. Note that this does not prevent SQL injection in the event that double-quoted literals happen to be used—it merely reduces the likelihood of that happening (because normal, non-malicious queries would fail).

In PDO, both its equivalent function PDO::quote() and its prepared statement emulator call upon mysql_handle_quoter()—which does exactly this: it ensures that the escaped literal is quoted in single-quotes, so you can be certain that PDO is always immune from this bug.

As of MySQL v5.7.6, this bug has been fixed. See change log:

Functionality Added or Changed

Safe Examples

Taken together with the bug explained by ircmaxell, the following examples are entirely safe (assuming that one is either using MySQL later than 4.1.20, 5.0.22, 5.1.11; or that one is not using a GBK/Big5 connection encoding):

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');

...because we've explicitly selected an SQL mode that doesn't include 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");

...because we're quoting our string literal with single-quotes.

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

...because PDO prepared statements are immune from this vulnerability (and ircmaxell's too, provided either that you're using PHP≥5.3.6 and the character set has been correctly set in the DSN; or that prepared statement emulation has been disabled).

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

...because PDO's quote() function not only escapes the literal, but also quotes it (in single-quote ' characters); note that to avoid ircmaxell's bug in this case, you must be using PHP≥5.3.6 and have correctly set the character set in the DSN.

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

...because MySQLi prepared statements are safe.

Wrapping Up

Thus, if you:

  • use native prepared statements

OR

  • use MySQL v5.7.6 or later

OR

  • in addition to employing one of the solutions in ircmaxell's summary, use at least one of:

    • PDO;
    • single-quoted string literals; or
    • an explicitly set SQL mode that does not include NO_BACKSLASH_ESCAPES

...then you should be completely safe (vulnerabilities outside the scope of string escaping aside).


Well, there's nothing really that can pass through that, other than % wildcard. It could be dangerous if you were using LIKE statement as attacker could put just % as login if you don't filter that out, and would have to just bruteforce a password of any of your users. People often suggest using prepared statements to make it 100% safe, as data can't interfere with the query itself that way. But for such simple queries it probably would be more efficient to do something like $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);

참고URL : https://stackoverflow.com/questions/5741187/sql-injection-that-gets-around-mysql-real-escape-string

반응형