การฉีด SQL ที่ได้รับรอบ mysql_real_escape_string ()


645

มีความเป็นไปได้ในการฉีด SQL แม้ว่าจะใช้mysql_real_escape_string()ฟังก์ชันอยู่หรือไม่

พิจารณาสถานการณ์ตัวอย่างนี้ 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 ข้างต้น?


34
@ThiefMaster - ฉันไม่ต้องการให้ข้อผิดพลาด verbose เช่นผู้ใช้ที่ไม่ถูกต้อง / รหัสผ่านไม่ถูกต้อง ... มันบอกพ่อค้าที่กำลังดุร้ายว่าพวกเขามี ID ผู้ใช้ที่ถูกต้องและเป็นเพียงรหัสผ่านที่พวกเขาต้องเดา
Mark Baker

18
มันน่ากลัวจากมุมมองการใช้งานแม้ว่า บางครั้งคุณไม่สามารถใช้ชื่อเล่น / ชื่อผู้ใช้ / ที่อยู่อีเมลหลักของคุณและลืมสิ่งนี้หลังจากผ่านไประยะหนึ่งหรือเว็บไซต์ถูกลบบัญชีของคุณเพื่อไม่ใช้งาน ถ้าหากคุณลองใช้รหัสผ่านต่อไปและอาจทำให้ IP ของคุณถูกบล็อกแม้ว่าชื่อผู้ใช้ของคุณจะไม่ถูกต้องก็ตาม
ThiefMaster

50
กรุณาอย่าใช้mysql_*ฟังก์ชั่นในรหัสใหม่ พวกเขาจะไม่ได้รับการบำรุงรักษาอีกต่อไปและกระบวนการเลิกใช้เริ่มต้นแล้ว เห็นกล่องสีแดง ? เรียนรู้เกี่ยวกับงบที่เตรียมไว้และใช้ PDOหรือ MySQLi -บทความนี้จะช่วยให้คุณตัดสินใจได้ว่า หากคุณเลือกที่ PDO,นี่คือการสอนที่ดี
tereško

13
@machineaddict ตั้งแต่ 5.5 (ซึ่งเปิดตัวเมื่อเร็ว ๆ นี้) mysql_*ฟังก์ชั่นสร้างE_DEPRECATEDคำเตือนแล้ว ext/mysqlนามสกุลยังไม่ได้รับการรักษามานานแล้ว 10 ปี คุณประสาทหลอนจริงๆเหรอ?
tereško

13
@machineaddict พวกเขาเพิ่งลบส่วนขยายนั้นบน PHP 7.0 และยังไม่เป็น 2050
GGG

คำตอบ:


379

พิจารณาคำถามต่อไปนี้:

$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";

9
แต่สิ่งนี้จะไม่เป็นปัญหาจริงเพราะmysql_query()ไม่ใช้คำสั่งหลายคำสั่งไม่ใช่หรือ?
Pekka

11
@Pekka แม้ว่าตัวอย่างตามปกติคือในทางปฏิบัติผู้โจมตีมีแนวโน้มที่จะDROP TABLE SELECT passwd FROM usersในกรณีหลังแบบสอบถามที่สองมักจะดำเนินการโดยใช้ส่วนUNIONคำสั่ง
Jacco

58
(int)mysql_real_escape_string- มันไม่สมเหตุสมผล ไม่แตกต่างจาก(int)ทั้งหมด และพวกเขาจะให้ผลลัพธ์เดียวกันสำหรับทุกอินพุต
zerkms

28
นี่เป็นการใช้งานในทางที่ผิดมากกว่าสิ่งอื่นใด หลังจากที่ทุกคนมันเป็นชื่อไม่ได้mysql_real_escape_string mysql_real_escape_integerมันไม่ได้หมายความว่าจะใช้กับเขตข้อมูลจำนวนเต็ม
NullUserException

11
@ircmaxell แต่คำตอบนั้นทำให้เข้าใจผิดโดยสิ้นเชิง เห็นได้ชัดว่าคำถามกำลังถามเกี่ยวกับเนื้อหาภายในเครื่องหมายคำพูด "คำพูดไม่ได้อยู่ที่นั่น" ไม่ใช่คำตอบสำหรับคำถามนี้
Pacerier

629

คำตอบสั้น ๆ คือใช่มีวิธีที่จะmysql_real_escape_string()แก้ไข

สำหรับกรณี EDBSURE มากขอบ !!!

คำตอบที่ยาวนั้นไม่ใช่เรื่องง่าย ก็ขึ้นอยู่การโจมตีแสดงให้เห็นที่นี่

การโจมตี

ดังนั้นเรามาเริ่มต้นด้วยการแสดงการโจมตี ...

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 แถว มาวิเคราะห์กันว่าเกิดอะไรขึ้นที่นี่:

  1. การเลือกชุดตัวละคร

    mysql_query('SET NAMES gbk');

    สำหรับการโจมตีการทำงานเราต้องเข้ารหัสที่เซิร์ฟเวอร์ของคาดหวังว่าในการเชื่อมต่อทั้งในการเข้ารหัส'ในขณะที่เช่น ASCII 0x27 และจะมีตัวละครบางคนที่มีไบต์สุดท้ายเป็น ASCII คือ\ 0x5cมันจะเปิดออกมี 5 การเข้ารหัสดังกล่าวได้รับการสนับสนุนใน MySQL 5.6 โดยค่าเริ่มต้น: big5, cp932, gb2312, และgbk sjisเราจะเลือกgbkที่นี่

    ตอนนี้มันสำคัญมากที่จะต้องสังเกตการใช้SET NAMESที่นี่ ชุดนี้ชุดอักขระบนเซิร์ฟเวอร์ ถ้าเราใช้การเรียกฟังก์ชัน C API mysql_set_charset()เราก็ไม่เป็นไร (ในรุ่น MySQL ตั้งแต่ปี 2549) แต่เพิ่มเติมเกี่ยวกับสาเหตุในไม่กี่นาที ...

  2. ส่วนของข้อมูล

    0xbf27น้ำหนักบรรทุกที่เรากำลังจะใช้สำหรับการฉีดนี้เริ่มต้นด้วยลำดับไบต์ ในgbkนั้นเป็นอักขระหลายไบต์ที่ไม่ถูกต้อง ในก็สตริงlatin1 ¿'โปรดสังเกตว่าในlatin1 และ gbk , 0x27ในตัวเองเป็นตัวอักษร'ตัวอักษร

    เราได้เลือกเพย์โหลดนี้เพราะถ้าเราเรียกaddslashes()มันเราจะใส่ ASCII \เช่น0x5cก่อน'ตัวละคร ดังนั้นเราจึงต้องการปิดท้ายด้วย0xbf5c27ซึ่งgbkเป็นลำดับสองตัวอักษร: ตามมาด้วย0xbf5c 0x27หรือคำอื่น ๆที่ถูกต้อง'ของตัวละครตามมาด้วยการไม่ใช้ Escape addslashes()แต่เราไม่ได้ใช้ ดังนั้นไปยังขั้นตอนต่อไป ...

  3. mysql_real_escape_string ()

    การเรียก C API เพื่อให้mysql_real_escape_string()แตกต่างจากaddslashes()ที่รู้ชุดอักขระการเชื่อมต่อ ดังนั้นจึงสามารถทำการหลบหนีได้อย่างถูกต้องสำหรับชุดอักขระที่เซิร์ฟเวอร์คาดหวัง อย่างไรก็ตามจนถึงจุดนี้ลูกค้าคิดว่าเรายังคงใช้latin1การเชื่อมต่ออยู่เพราะเราไม่เคยบอกเป็นอย่างอื่น เราไม่ได้บอกเซิร์ฟเวอร์ที่เรากำลังใช้gbkแต่ลูกค้าlatin1ยังคงคิดว่ามันเป็น

    ดังนั้นการเรียกให้mysql_real_escape_string()ใส่แบ็กสแลชและเรามี'ตัวอักษรลอยฟรีในเนื้อหา "หนี" ของเรา! อันที่จริงถ้าเราต้องดู$varในgbkชุดตัวละครเราจะได้เห็น:

    縗 'หรือ 1 = 1 / *

    ซึ่งเป็นสิ่งที่การโจมตีต้องการ

  4. คำค้นหา

    ส่วนนี้เป็นเพียงพิธีการ แต่นี่คือแบบสอบถามที่แสดงผล:

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

ขอแสดงความยินดีคุณเพิ่งประสบความสำเร็จในการโจมตีโปรแกรมโดยใช้mysql_real_escape_string()...

เลว

มันแย่ลงเรื่อย ๆ PDOค่าเริ่มต้นในการเลียนแบบคำสั่งที่เตรียมไว้ด้วย MySQL นั่นหมายความว่าในฝั่งไคลเอ็นต์จะทำการ sprintf 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และนั่นเป็นความจริงหากคุณใช้ MySQL รุ่นตั้งแต่ปี 2549

หากคุณกำลังใช้รุ่น MySQL ก่อนหน้านี้แล้วข้อผิดพลาดในmysql_real_escape_string()ความหมายว่าตัวอักษรสัญลักษณ์ที่ไม่ถูกต้องเช่นผู้ที่อยู่ในส่วนของข้อมูลของเราได้รับการรักษาเป็นไบต์เดียวสำหรับวัตถุประสงค์ในการหลบหนีแม้กระทั่งในกรณีที่ลูกค้าได้รับแจ้งอย่างถูกต้องของการเข้ารหัสการเชื่อมต่อและการโจมตีครั้งนี้จะ ยังคงประสบความสำเร็จ ข้อผิดพลาดได้รับการแก้ไขใน MySQL 4.1.20 , 5.0.22และ5.1.11

แต่ส่วนที่แย่ที่สุดคือPDOไม่ได้เปิดเผย C API เป็นเวลาmysql_set_charset()จนถึง 5.3.6 ดังนั้นในเวอร์ชันก่อนหน้านี้จึงไม่สามารถป้องกันการโจมตีนี้สำหรับทุกคำสั่งที่เป็นไปได้! มันสัมผัสในขณะนี้เป็นพารามิเตอร์ DSN

พระคุณประหยัด

ดังที่เราได้กล่าวไว้ในตอนต้นสำหรับการโจมตีครั้งนี้เพื่อการทำงานการเชื่อมต่อฐานข้อมูลจะต้องเข้ารหัสโดยใช้ชุดอักขระที่มีช่องโหว่ utf8mb4คือไม่เสี่ยงและยังสามารถรองรับทุกอักขระ Unicode: เพื่อให้คุณสามารถเลือกที่จะใช้ที่แทน แต่จะได้รับเพียงมีตั้งแต่ MySQL 5.5.3 อีกทางเลือกหนึ่งคือutf8ซึ่งไม่เสี่ยงและสามารถรองรับ Unicode Basic Multilingual Planeทั้งหมด

หรือคุณสามารถเปิดใช้งานNO_BACKSLASH_ESCAPESโหมด SQL ซึ่ง (ในหมู่สิ่งอื่น ๆ ) alters mysql_real_escape_string()การดำเนินงานของ เมื่อเปิดใช้งานโหมดนี้0x27จะถูกแทนที่ด้วย0x2727มากกว่า0x5c27และทำให้กระบวนการหลบหนีไม่สามารถสร้างตัวละครที่ถูกต้องในการเข้ารหัสที่มีช่องโหว่ที่ไม่เคยมีอยู่ก่อนหน้า (เช่น0xbf27ยัง0xbf27เป็นต้น) - ดังนั้นเซิร์ฟเวอร์จะปฏิเสธสตริงที่ไม่ถูกต้อง . อย่างไรก็ตามดูคำตอบของ @ eggyalสำหรับช่องโหว่อื่นที่อาจเกิดขึ้นจากการใช้โหมด SQL นี้

ตัวอย่างที่ปลอดภัย

ตัวอย่างต่อไปนี้ปลอดภัย:

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 charset (ใน PHP ≥ 5.3.6)

หรือ

  • อย่าใช้ชุดอักขระที่มีช่องโหว่สำหรับการเข้ารหัสการเชื่อมต่อ (คุณใช้เพียงutf8/ latin1/ ascii/ ฯลฯ )

คุณปลอดภัย 100%

ไม่อย่างนั้นคุณก็มีช่องโหว่ถึงแม้ว่าคุณจะใช้mysql_real_escape_string() ...


3
PDO ลอกเลียนแบบการจัดทำงบสำหรับ MySQL จริงเหรอ? ฉันไม่เห็นเหตุผลที่จะทำเช่นนั้นเนื่องจากไดรเวอร์สนับสนุนอย่างเป็นทางการ ไม่มี?
netcoder

16
มันทำ พวกเขาพูดในเอกสารไม่ได้ แต่ในซอร์สโค้ดมันเห็นได้ชัดและง่ายต่อการแก้ไข ฉันชอล์กมันถึงความสามารถของ devs
Theodore R. Smith

5
@ TheodoreR.Smith: ไม่ใช่เรื่องง่ายที่จะแก้ไข ฉันทำงานเกี่ยวกับการเปลี่ยนค่าเริ่มต้น แต่มันไม่สามารถโหลดการทดสอบเรือได้เมื่อเปลี่ยน ดังนั้นมันจึงเป็นการเปลี่ยนแปลงที่ยิ่งใหญ่กว่าที่ปรากฏ ฉันยังคงหวังว่าจะเสร็จประมาณ 5.5 ...
ircmaxell

14
@shadyyx: addslashesไม่มีช่องโหว่บทความที่อธิบายเป็นเรื่องเกี่ยวกับ ฉันใช้ช่องโหว่นี้กับสิ่งนั้น ลองด้วยตัวคุณเอง ไปรับ MySQL 5.0 และเรียกใช้ประโยชน์จากนี้และดูด้วยตัวคุณเอง เท่าที่จะใส่ลงไปใน PUT / GET / POST มันเป็นเรื่องสำคัญ ข้อมูลอินพุตเป็นเพียงสตรีมไบต์ char(0xBF)เป็นเพียงวิธีการสร้างไบต์ ฉันได้สาธิตช่องโหว่นี้แล้วแบบสดๆต่อหน้าการประชุมหลายครั้ง เชื่อใจฉันในเรื่องนี้ ... แต่ถ้าคุณไม่ลองด้วยตัวเอง มันใช้งานได้ ...
ircmaxell

5
@shadyyx: สำหรับการส่งความสนุกใน $ _GET ... ?var=%BF%27+OR+1=1+%2F%2Aใน URL $var = $_GET['var'];ในโค้ดและบ๊อบเป็นลุงของคุณ
cHao

183

TL; DR

mysql_real_escape_string()จะไม่ให้การคุ้มครองใด ๆ (และอาจทำให้ข้อมูลของคุณเสียหาย) หาก:

  • ของ MySQL NO_BACKSLASH_ESCAPESโหมด SQL ถูกเปิดใช้งาน (ซึ่งอาจจะเป็นไปได้จนกว่าคุณอย่างชัดเจนเลือกโหมด SQL อีกทุกครั้งที่คุณเชื่อมต่อ ); และ

  • ตัวอักษรของสตริง SQL ของคุณจะถูกยกมาใช้อ้างดับเบิล"ตัวอักษร

สิ่งนี้ถูกยื่นเป็นข้อผิดพลาด # 72458และได้รับการแก้ไขใน MySQL v5.7.6 (ดูหัวข้อ " The Saving Grace " ด้านล่าง)

นี่เป็นอีกกรณีหนึ่ง (อาจน้อยกว่านี้) ปิดบัง EDGE !!!

เป็นการแสดงความเคารพต่อคำตอบที่ยอดเยี่ยมของ @ 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"');

    ตามที่จัดทำภายใต้String Literals :

    มีหลายวิธีในการรวมอักขระเครื่องหมายคำพูดไว้ในสตริง:

    • '” ภายในสตริงที่ยกมาด้วย“ '” อาจเขียนเป็น“ ''

    • "” ภายในสตริงที่ยกมาด้วย“ "” อาจเขียนเป็น“ ""

    • นำหน้าอักขระเครื่องหมายคำพูดโดยอักขระเลี่ยง (“ \”)

    • คำว่า“ '” ภายในสตริงที่ยกมาด้วย“ "” ไม่จำเป็นต้องได้รับการดูแลเป็นพิเศษและไม่จำเป็นต้องเพิ่มเป็นสองเท่าหรือหลบหนี ในทำนองเดียวกัน“ "” ภายในสตริงที่ยกมาด้วย“ '” ไม่จำเป็นต้องได้รับการดูแลเป็นพิเศษ

    หากโหมด SQL ของเซิร์ฟเวอร์รวมNO_BACKSLASH_ESCAPESอยู่ตัวเลือกที่สามของตัวเลือกเหล่านี้ - ซึ่งเป็นวิธีการปกติที่ใช้โดยmysql_real_escape_string()ไม่พร้อมใช้งาน: หนึ่งในสองตัวเลือกแรกจะต้องใช้แทน โปรดทราบว่าผลกระทบของสัญลักษณ์แสดงหัวข้อย่อยที่สี่คือว่าเราจะต้องรู้ว่าตัวละครที่จะต้องใช้ในการอ้างอิงตัวอักษรเพื่อหลีกเลี่ยงการ munging ข้อมูล

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

    ดังนั้นฟังก์ชั่นพื้นฐานที่แตกต่างกันescape_quotes_for_mysql()จะถูกเรียกใช้หากใช้NO_BACKSLASH_ESCAPESโหมด SQL ดังกล่าวข้างต้นฟังก์ชั่นดังกล่าวจำเป็นต้องรู้ว่าตัวละครใดที่จะใช้ในการอ้างอิงตัวอักษรเพื่อที่จะทำซ้ำโดยไม่ทำให้ตัวละครคำพูดอื่น ๆ จากการถูกทำซ้ำอย่างแท้จริง

    อย่างไรก็ตามฟังก์ชั่นนี้จะถือว่าสตริงนั้นจะถูกยกมาโดยใช้'ตัวอักษรคำพูดเดียว ดู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()-IT ราวกับว่าไม่มีหนีได้ที่สถานที่ทั้งหมด

  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อาจไม่ทั้งหมดที่ผิดปกติใน wild เนื่องจากความจำเป็นในการใช้งานร่วมกับ SQL มาตรฐาน (เช่นดูส่วนที่ 5.3 ของข้อกำหนดSQL-92 , การ<quote symbol> ::= <quote><quote>ผลิตไวยากรณ์และการขาดความหมายพิเศษที่กำหนดให้ backslash) นอกจากนี้การใช้งานก็แนะนำอย่างชัดเจนว่าเป็นวิธีแก้ปัญหาข้อผิดพลาด (ยาวตั้งแต่แก้ไข) ที่โพสต์ของ ircmaxell อธิบาย ใครจะรู้ DBAs addslashes()บางคนยังอาจกำหนดค่าให้เป็นตามค่าเริ่มต้นเป็นวิธีการท้อใจการใช้วิธีการที่ไม่ถูกต้องเช่นการหลบหนี

นอกจากนี้โหมด SQL ของการเชื่อมต่อใหม่จะถูกกำหนดโดยเซิร์ฟเวอร์ตามการกำหนดค่า (ซึ่งSUPERผู้ใช้สามารถเปลี่ยนแปลงได้ตลอดเวลา); จึงจะต้องมีบางอย่างของพฤติกรรมของเซิร์ฟเวอร์คุณต้องเสมอระบุอย่างชัดเจนโหมดที่ต้องการของคุณหลังจากที่เชื่อมต่อ

พระคุณประหยัด

ตราบใดที่คุณมักจะชัดเจนตั้งค่าโหมด SQL ที่จะไม่รวมถึงNO_BACKSLASH_ESCAPESหรือพูด MySQL อักษรสตริงที่ใช้ตัวอักษรตัวเดียวอ้างข้อผิดพลาดนี้ไม่สามารถด้านหลังหัวน่าเกลียดของมันตามลำดับescape_quotes_for_mysql()จะไม่ถูกใช้หรือสมมติฐานเกี่ยวกับตัวอักษรที่อ้างจำเป็นต้องมีความประสงค์การทำซ้ำ ถูกต้อง

ด้วยเหตุนี้ฉันขอแนะนำให้ทุกคนที่ใช้โหมดNO_BACKSLASH_ESCAPESเปิดANSI_QUOTESใช้งานด้วยเช่นกันเพราะจะบังคับให้ใช้ตัวอักษรสตริงที่อ้างถึงเดี่ยว โปรดทราบว่าสิ่งนี้ไม่ได้ป้องกันการฉีด SQL ในกรณีที่มีการใช้ตัวอักษรที่มีเครื่องหมายคำพูดคู่ - เพียง แต่ลดโอกาสในการเกิดเหตุการณ์นั้น (เนื่องจากการสืบค้นธรรมดาหรือไม่ประสงค์ร้ายจะล้มเหลว)

ใน PDO ทั้งฟังก์ชันที่เทียบเท่าPDO::quote()และอีมูเลเตอร์คำสั่งที่เตรียมไว้จะเรียกmysql_handle_quoter()- ซึ่งทำสิ่งนี้: ทำให้มั่นใจว่าตัวอักษรที่ถูกหลบหนีถูกอ้างถึงในเครื่องหมายคำพูดเดี่ยวดังนั้นคุณจึงมั่นใจได้ว่า PDO จะรอดพ้นจากข้อผิดพลาดนี้

ในฐานะของ MySQL v5.7.6 ข้อผิดพลาดนี้ได้รับการแก้ไข ดูบันทึกการเปลี่ยนแปลง :

เพิ่มหรือเปลี่ยนฟังก์ชั่นการทำงาน

  • การเปลี่ยนแปลงที่เข้ากันไม่ได้:ฟังก์ชั่น C API ใหม่mysql_real_escape_string_quote()ได้ถูกนำมาใช้แทนmysql_real_escape_string()เพราะฟังก์ชั่นหลังอาจไม่สามารถเข้ารหัสอักขระได้อย่างถูกต้องเมื่อNO_BACKSLASH_ESCAPESเปิดใช้งานโหมด SQL ในกรณีนี้mysql_real_escape_string()ไม่สามารถหลีกเลี่ยงอักขระเครื่องหมายคำพูดยกเว้นโดยเพิ่มเป็นสองเท่าและเพื่อดำเนินการอย่างถูกต้องจะต้องทราบข้อมูลเพิ่มเติมเกี่ยวกับบริบทการอ้างอิงมากกว่าที่มีอยู่mysql_real_escape_string_quote()รับอาร์กิวเมนต์พิเศษเพื่อระบุบริบทการอ้างอิง สำหรับรายละเอียดการใช้งานดูmysql_real_escape_string_quote ()

     บันทึก

    ควรแก้ไขแอปพลิเคชันให้ใช้mysql_real_escape_string_quote()แทนmysql_real_escape_string()ซึ่งตอนนี้ล้มเหลวและสร้างCR_INSECURE_API_ERRข้อผิดพลาดหากNO_BACKSLASH_ESCAPESเปิดใช้งาน

    ข้อมูลอ้างอิง: ดูเพิ่มเติม Bug # 19211994

ตัวอย่างที่ปลอดภัย

นำมาพร้อมกับข้อผิดพลาดที่อธิบายโดย ircmaxell ตัวอย่างต่อไปนี้มีความปลอดภัยอย่างสมบูรณ์ (สมมติว่าเป็นอย่างใดอย่างหนึ่งที่ใช้ MySQL หลังจาก 4.1.20, 5.0.22, 5.1.11 หรือว่าหนึ่งไม่ได้ใช้การเข้ารหัสการเชื่อมต่อ 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 นั้นได้รับการยกเว้นจากช่องโหว่นี้ (และ ircmaxell ด้วยเช่นกันหากคุณใช้PHP≥5.3.6และชุดอักขระได้รับการตั้งค่าอย่างถูกต้องใน DSN หรือว่าการจำลองคำสั่งที่เตรียมไว้ถูกปิดใช้งาน) .

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

... เพราะquote()ฟังก์ชั่นของ PDO ไม่เพียง แต่จะช่วยหลบหนี แต่ยังรวมถึงคำพูด (ใน'ตัวอักษรคำพูดเดียว); โปรดทราบว่าเพื่อหลีกเลี่ยงข้อผิดพลาดของ 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

... ถ้าอย่างนั้นคุณควรจะปลอดภัยอย่างสมบูรณ์ (ช่องโหว่ที่อยู่นอกขอบเขตของสายอักขระที่หลบหนี)


10
ดังนั้น TL; DR จะเป็นแบบ "มีโหมดเซิร์ฟเวอร์ NO_BACKSLASH_ESCAPES mysql ซึ่งสามารถทำให้เกิดการฉีดได้หากคุณไม่ได้ใช้เครื่องหมายคำพูดเดี่ยว
สามัญสำนึกของคุณ

ฉันไม่สามารถเข้าถึงbugs.mysql.com/bug.php?id=72458 ; ฉันเพิ่งได้รับหน้าปฏิเสธการเข้าถึง มันถูกซ่อนจากสาธารณะเนื่องจากปัญหาด้านความปลอดภัยหรือไม่? นอกจากนี้ฉันเข้าใจอย่างถูกต้องจากคำตอบนี้ว่าคุณเป็นผู้ค้นพบช่องโหว่หรือไม่? ถ้าเป็นเช่นนั้นขอแสดงความยินดี
Mark Amery

1
ผู้คนไม่ควรใช้"สตริงในตอนแรก SQL บอกว่าสำหรับตัวระบุ แต่เอ๊ะ ... แค่อีกตัวอย่างหนึ่งของ MySQL ที่พูดว่า "มาตรฐานสกรูฉันจะทำทุกอย่างที่ฉันต้องการ" (โชคดีที่คุณสามารถรวมANSI_QUOTESอยู่ในโหมดเพื่อแก้ไขข้อบกพร่องที่อ้างถึงแม้ว่าการเพิกเฉยต่อมาตรฐานแบบเปิดเป็นปัญหาที่ใหญ่กว่าที่อาจต้องใช้มาตรการที่รุนแรงกว่านี้)
cHao

2
@DanAllen: คำตอบของฉันกว้างขึ้นเล็กน้อยในการที่คุณสามารถหลีกเลี่ยงข้อผิดพลาดนี้ผ่านquote()ฟังก์ชั่นของ PDO แต่ข้อความที่เตรียมไว้เป็นวิธีที่ปลอดภัยและเหมาะสมกว่าในการหลีกเลี่ยงการฉีดยา แน่นอนว่าถ้าคุณมีตัวแปรที่ไม่ได้ใส่ค่าลงใน SQL เข้าด้วยกันโดยตรงคุณก็จะมีความเสี่ยงต่อการฉีดไม่ว่าคุณจะใช้วิธีการใดในภายหลัง
eggyal

1
@eggyall: ระบบของเราอาศัยตัวอย่างความปลอดภัยที่ 2 ด้านบน มีข้อผิดพลาดที่ไม่ได้ระบุ mysql_real_escape_string การแก้ไขผู้ที่อยู่ในโหมดฉุกเฉินดูเหมือนว่าจะเป็นเส้นทางที่รอบคอบหวังว่าเราจะไม่ได้รับการแก้ไขก่อนการแก้ไข เหตุผลของฉันคือการแปลงไปสู่แถลงการณ์ที่เตรียมไว้จะเป็นกระบวนการที่ยาวนานกว่ามากซึ่งจะต้องเกิดขึ้นในภายหลัง เหตุผลที่จัดทำขึ้นนั้นปลอดภัยกว่าข้อเท็จจริงที่ว่าข้อผิดพลาดไม่ได้สร้างช่องโหว่หรือไม่? ในคำอื่น ๆ จะถูกนำไปใช้อย่างถูกต้องตัวอย่างที่ 2 ข้างต้นมีความปลอดภัยเช่นเดียวกับงบเตรียม?
DanAllen

18

มีอะไรที่สามารถผ่านได้นอกเหนือจาก%ไวด์การ์ด อาจเป็นอันตรายได้หากคุณใช้LIKEคำสั่งในฐานะผู้โจมตีอาจใส่เพียงแค่การ%เข้าสู่ระบบหากคุณไม่ได้กรองและจะต้องใส่รหัสผ่านของผู้ใช้ของคุณ ผู้คนมักจะแนะนำให้ใช้คำสั่งที่เตรียมไว้เพื่อให้ปลอดภัย 100% เนื่องจากข้อมูลไม่สามารถเข้าไปยุ่งกับแบบสอบถามได้ แต่สำหรับการค้นหาแบบง่าย ๆ มันอาจจะมีประสิทธิภาพมากกว่าที่จะทำสิ่งที่ชอบ$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);


2
+1 แต่อักขระตัวแทนใช้สำหรับคำสั่ง LIKE ไม่ใช่ความเท่าเทียมกันอย่างง่าย
Dor

7
คุณพิจารณาถึงการเปลี่ยนที่ง่ายmore efficientกว่าการวัดโดยใช้คำสั่งที่เตรียมไว้โดยวิธีใด (งบเตรียมเสมอทำงานห้องสมุดที่สามารถแก้ไขได้อย่างรวดเร็วในกรณีของการโจมตีไม่เปิดเผยผิดพลาดของมนุษย์ [เช่นผิดพลาดการพิมพ์ที่สมบูรณ์แทนที่สตริง] และมีประโยชน์ต่อการปฏิบัติงานที่สำคัญหากคำสั่งเป็นอีกครั้งที่ใช้.)
MatBailie

7
@Slava: คุณ จำกัด ชื่อผู้ใช้และรหัสผ่านให้กับตัวอักษรคำได้อย่างมีประสิทธิภาพเท่านั้น คนส่วนใหญ่ที่รู้อะไรเกี่ยวกับความปลอดภัยจะพิจารณาว่าความคิดที่ไม่ดีเพราะมันทำให้พื้นที่การค้นหาลดลงอย่างมาก หลักสูตรที่พวกเขาคิดว่าเป็นความคิดที่ดีเช่นกันในการจัดเก็บรหัสผ่าน cleartext ในฐานข้อมูล แต่เราไม่จำเป็นต้องทบต้นปัญหา :)
cHao

2
@cHao ข้อเสนอแนะของฉันเกี่ยวข้องกับการเข้าสู่ระบบเท่านั้น เห็นได้ชัดว่าคุณไม่จำเป็นต้องกรองรหัสผ่านขออภัยคำตอบของฉันไม่ได้ระบุไว้อย่างชัดเจน แต่จริงๆแล้วอาจเป็นความคิดที่ดี การใช้ "พื้นที่ต้นไม้หินงมงาย" แทนการจำได้ยากและพิมพ์ "a4üua3! @v \" ä90; 8f "จะเป็นการยากยิ่งที่จะ bruteforce แม้จะใช้พจนานุกรมพูด 3000 คำเพื่อช่วยให้คุณรู้ คุณใช้คำที่ถูกต้อง 4 คำ - ซึ่งยังคงเป็นชุดค่าผสมประมาณ 3.3 * 10 ^ 12 :)
Slava

2
@Slava: ฉันเคยเห็นความคิดนั้นมาก่อน ดูxkcd.com/936 ปัญหาคือคณิตศาสตร์ไม่ได้ค่อนข้างทน ตัวอย่างรหัสผ่าน 17-char ของคุณจะมีความเป็นไปได้ที่ 96 ^ 17 และนั่นคือถ้าคุณลืมเครื่องหมาย umlauts และ จำกัด ตัวเองให้พิมพ์ ASCII ประมาณ 4.5x10 ^ 33 เรากำลังพูดถึงงานมากกว่าหนึ่งแสนล้านล้านเท่าเพื่อเพิ่มพลัง แม้แต่รหัสผ่าน ASCII แบบ 8 ถ่านก็มีความเป็นไปได้ 7.2x10 ^ 15 - มากกว่า 3,000 ครั้ง
cHao
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.