PHP安全漏洞剖析:addslashes与str_replace组合的SQL注入绕过

发布时间:2026/7/5 0:06:12
PHP安全漏洞剖析:addslashes与str_replace组合的SQL注入绕过 1. 引子当转义遇上替换一场精心设计的“安全”陷阱最近在复盘一些经典的CTF题目特别是Web安全方向的发现很多题目设计的精妙之处不在于用了多么高深的技术而在于对基础函数特性的“组合拳”应用。今天想和大家深入聊聊CISCN2019华北赛区Day2 Web1这道名为“Hack World”的题目。它表面上看起来是一个简单的布尔盲注但内核却巧妙地利用了PHP中两个非常基础、开发者几乎天天在用的函数——addslashes()和str_replace()——之间的微妙关系构建了一个看似安全实则脆弱的防御层。很多朋友在初次接触时可能会觉得过滤很严无从下手但一旦理解了这两个函数在处理特定字符时的“相爱相杀”整个绕过思路就会豁然开朗。这道题之所以经典是因为它完美地模拟了一个现实场景开发者在处理用户输入时常常会进行多层过滤和转义认为这样就能高枕无忧。他们可能会先用str_replace过滤掉一些明显的危险字符比如单引号、双引号然后再用addslashes进行转义心想“这下总该安全了吧”。但安全的世界里11有时候并不等于2甚至可能小于1。这种组合非但没有形成叠加的防御反而因为处理顺序和字符编码的细节留下了一个可以利用的缝隙。我们今天就从这个缝隙钻进去看看里面到底藏着什么玄机以及如何利用它完成一次漂亮的SQL注入绕过。无论你是正在学习Web安全的新手还是想深入理解PHP安全特性的老手相信这个案例都能给你带来一些启发。2. 题目环境与核心逻辑复原首先我们需要在本地或测试环境中复原题目的核心逻辑。虽然我们拿不到原题的完整源码但根据题目描述和常见的出题模式我们可以推断出其后端PHP代码的关键部分。通常这类题目会有一个简单的输入框提交一个id参数后端根据这个id查询数据库并返回结果。一个高度简化的、模拟题目逻辑的PHP代码片段可能如下所示?php // 模拟数据库连接实际题目中可能是SQLite或MySQL $flag flag{this_is_a_fake_flag}; $servername localhost; $username test; $password test; $dbname testdb; // 接收用户输入 $id $_GET[id]; // 第一层过滤使用str_replace尝试删除单引号和双引号 $filtered_id str_replace(array(, ), , $id); // 第二层防御使用addslashes进行转义 $safe_id addslashes($filtered_id); // 构造SQL查询语句 $sql SELECT * FROM articles WHERE id . $safe_id; // 或者可能是 $sql SELECT * FROM articles WHERE id $safe_id; echo 执行的SQL语句: . $sql . br/; // 模拟查询逻辑... // 这里通常会有一个数据库查询操作并根据结果返回不同页面内容True/False形成布尔盲注的条件 ?核心逻辑拆解输入获取从$_GET[‘id’]获取用户输入的id值。第一层处理str_replace使用str_replace(array(“‘“, ‘“‘), ”, $id)意图是将输入中的单引号‘和双引号“直接删除替换为空字符串。开发者的想法是“我先把你可能用来闭合语句的引号干掉看你怎么注入”。第二层处理addslashes对经过str_replace处理后的字符串$filtered_id再使用addslashes()函数进行转义。addslashes会在预定义的字符单引号‘、双引号“、反斜线\和NULL字符前添加反斜线\进行转义。SQL拼接将处理后的“安全”的$safe_id拼接到SQL查询语句中。开发者的心路历程大概是“我先过滤掉引号再转义一遍双保险”。但问题就出在这个处理顺序和具体函数的特性上。我们需要深入理解这两个函数的行为才能找到破绽。3. 关键函数特性深度剖析addslashes与str_replace的“特性”3.1 addslashes()它到底转义了什么addslashes(string $str): string这个函数大家太熟悉了它的官方定义是在指定的预定义字符前添加反斜杠。这些预定义字符是单引号 (‘)双引号 (“)反斜杠 (\)NULLNUL字符即ASCII码为0的字符例如$str OReilly; echo addslashes($str); // 输出O\Reilly这看起来没问题在SQL中O\Reilly中的\‘会被解释为一个字面量的单引号字符而不是字符串的结束符。但这里有一个至关重要的、容易被忽略的细节addslashes转义的是字符本身而不是它的某种编码表示。当我们通过HTTP GET/POST方法传递参数时浏览器和服务器会对一些字符进行URL编码。例如单引号‘的URL编码是%27空字符\0的URL编码是%00。addslashes不会识别%27为一个单引号。它只会在它扫描字符串时遇到字面上的‘字符ASCII码39时才在其前面加反斜杠。如果你传入的是%27addslashes会把它当作三个普通字符%、2、7来处理不会进行转义。3.2 str_replace()它是如何“删除”的str_replace(mixed $search, mixed $replace, mixed $subject): mixed函数用于在字符串中替换一些值。在我们的模拟代码中str_replace(array(“‘“, ‘“‘), ”, $id)意味着在$id字符串中查找所有出现的字面量单引号‘和双引号“并将它们替换为空字符串即删除。关键点在于它删除的也是字面上的引号字符。如果输入是%27str_replace同样会将其视为三个字符%、2、7其中没有任何一个字符等于‘或“所以它不会做任何删除操作。3.3 危险的组合顺序与编码的博弈现在我们把两个函数组合起来看假设用户原始输入是%27即URL编码后的单引号。$id ‘%27’;(PHP接收到的是解码后的%27字符串还是原始的%27字符这里取决于PHP配置magic_quotes_gpc等但现代PHP默认已不再自动解码我们通常直接获得%27这个字符串)。$filtered_id str_replace(array(“‘“, ‘“‘), ”, ‘%27’);检查%27里面没有字面量‘所以原样返回%27。$safe_id addslashes(‘%27’);检查%27里面没有字面量‘所以原样返回%27。$sql “SELECT * FROM articles WHERE id ‘” . $safe_id . “‘”;最终SQL为SELECT * FROM articles WHERE id ‘%27‘。看到了吗一个编码后的单引号%27成功地穿过了两层“防御”完整地进入了SQL语句。当这个SQL语句被数据库执行时数据库的解析器会将%27解码为一个字面量的单引号‘因为%27在SQL字符串的上下文中就是代表一个单引号字符。于是我们成功地在SQL语句中植入了一个未转义的单引号从而可以闭合原有的字符串开始进行注入。但这道题CISCN2019 Hack World的绕过关键点还不是%27而是另一个更隐蔽的角色——空字符\0 ASCII 0。4. 核心绕过原理空字符\0的魔法题目真正的考点在于addslashes和str_replace对空字符\0的处理差异。这也是“相爱相杀”这个说法最精髓的体现。4.1 addslashes 对 \0 的行为根据PHP手册addslashes确实会转义\0NULL字符。它会将\0转义为\\0一个反斜杠后跟一个ASCII 0字符。例如$str “abc\0def”; echo bin2hex(addslashes($str)); // 输出6162635c30646566 // ‘a’(61) ‘b’(62) ‘c’(63) ‘\’(5c) ‘0’(30) ‘d’(64) ‘e’(65) ‘f’(66)注意这里的0是数字字符0ASCII 48而不是NULL。addslashes在NULL字符前加了一个反斜杠\。4.2 str_replace 对 \0 的行为str_replace在查找要替换的字符时如果搜索字符串中包含\0它会把\0当作字符串的终止符吗或者能正确识别并替换它吗这是一个关键。实际上在PHP的字符串处理中\0确实有特殊含义表示字符串结束C语言风格。但str_replace函数内部是如何处理包含\0的搜索字符串的呢我们做个小实验// 测试1搜索字符串中包含字面量 \0 $subject “123\0abc”; $search “\0”; $result str_replace($search, “”, $subject); echo “Result: “ . bin2hex($result) . “\n”; // 输出Result: 313233616263 // 解释成功删除了 \0 (00)结果是 ‘1’‘2’‘3’‘a’‘b’‘c’ // 测试2搜索数组里包含 \0 $subject “123\0abc\0def”; $search array(“\0”); $result str_replace($search, “”, $subject); echo “Result: “ . bin2hex($result) . “\n”; // 输出Result: 313233616263646566 // 解释成功删除了所有的 \0实验表明str_replace可以正确识别并删除字符串中的\0字符。4.3 构造Payload利用处理顺序的漏洞现在我们来看攻击链条。假设用户输入中包含一个空字符\0其URL编码为%00。攻击Payload构思我们输入%00%27即\0‘。注意在HTTP传输中我们提交的是字面字符%00%27。接收与解码PHP接收到参数id%00%27。假设$_GET[‘id’]得到的是字符串“%00%27”注意这里%00和%27还没有被URL解码成二进制字符它们就是三个字符%、0、0和%、2、7。但在PHP的某些上下文或经过urldecode后它可能被解释为包含两个字节的字符串第一个字节是\0第二个字节是‘。为了触发漏洞我们需要确保在str_replace处理时它已经被解码成二进制形式。通常PHP在填充$_GET时会对参数值进行URL解码。所以我们假设$id的值已经是二进制字符串“\0‘”十六进制0027。第一层 str_replacestr_replace(array(“‘“, ‘“‘), ”, “\0‘”)。它会查找字面量的‘并删除。于是字符串“\0‘”中的‘被删除变成了“\0”十六进制00。第二层 addslashesaddslashes(“\0”)。根据规则它会在\0前添加反斜杠进行转义。所以“\0”变成了“\\0”十六进制5c30。注意这里的0是数字字符0ASCII 48。SQL拼接假设SQL语句是$sql “SELECT * FROM table WHERE id‘” . $safe_id . “‘”;。那么最终的SQL语句是SELECT * FROM table WHERE id‘\\0‘这里的\\0在SQL字符串中会被解释为一个反斜杠字符\后跟一个数字字符0。漏洞出现了我们最初输入的是\0‘目标是让单引号‘逃逸。经过str_replace‘被删了只剩\0。再经过addslashes\0被转义成\\0。最终SQL语句里的字符串是‘\\0‘。我们成功地将一个反斜杠\和字符0插入了数据库的字符串中但是我们最初想引入的单引号‘呢它好像不见了别急真正的利用方式需要更精巧的构造。经典的Payload是0%bf%27。这里用到了另一个特性宽字节注入GBK等字符集的变种或者利用addslashes转义特定字符后产生的多字节字符组合。但在Hack World这道题中更直接的利用方式与\0和str_replace的顺序有关。核心洞见如果str_replace的搜索数组里包含了空字符\0呢题目源码很可能是这样的str_replace(array(“‘“, ‘“‘, ‘\0‘), ”, $id)或者由于\0在字符串中难以直接书写可能用chr(0)表示。意图是把单引号、双引号和空字符都删掉彻底杜绝这些特殊字符。但这里存在一个逻辑悖论如果str_replace先执行它删除了\0。然后addslashes后执行它需要转义\0但此时\0已经被删除了所以addslashes无事可做。最终一个原始的、未转义的\0可能因为处理顺序而“消失”了但addslashes转义其他字符如单引号的行为可能被干扰。实际上更常见的利用模式是输入一个经过编码的、能使addslashes转义后产生\0字符的序列。例如在MySQL中%bf%27是一个经典的组合。%bf本身是一个非法多字节字符的头字节。当addslashes在%27单引号前插入反斜杠\%5c时就形成了%bf%5c%27。在某些特定的字符集如GBK下%bf%5c可能被解释为一个合法的宽字节字符从而“吃掉”了反斜杠使得后面的%27单引号逃脱了转义成为有效的SQL语句闭合符。而在Hack World这道题中结合str_replace会删除\0的特性攻击者可以构造一个Payload使得经过addslashes转义后的字符串中包含一个\0字符然后寄希望于后续的字符串处理或SQL解析中这个\0会被当作终止符从而截断后面的转义符或引号达到绕过目的。不过根据公开的Writeup这道题更常见的解法是利用布尔盲注和str_replace过滤空格或某些关键词的特性通过双写、注释符/**/等方式绕过而\0的利用可能是其中一种变种或前置条件。为了彻底搞懂我们直接进入实战看看如何针对复原的代码逻辑进行攻击。5. 实战攻击一步步实现SQL注入绕过让我们基于之前复原的代码逻辑设计一个具体的、可复现的攻击场景。假设后端代码严格如下$id $_GET[‘id’]; $filtered_id str_replace(array(“‘“, ‘“‘, chr(0)), ”, $id); // 明确过滤了单引号、双引号和空字符 $safe_id addslashes($filtered_id); $sql “SELECT * FROM users WHERE id‘” . $safe_id . “‘ AND status1”; // 执行查询...5.1 攻击目标分析我们的目标是注入SQL代码改变原语句的逻辑。原语句是SELECT * FROM users WHERE id‘$safe_id‘ AND status1我们希望注入后能忽略掉AND status1这个条件或者能执行UNION查询等。由于有addslashes我们无法直接使用单引号闭合字符串。由于有str_replace直接输入单引号、双引号、空字符会被删除。5.2 构造绕过Payload我们需要找到一个输入X使得经过str_replace和addslashes处理后X能变成一个有效的单引号‘或者能提前闭合字符串。思路一利用宽字节注入如果数据库连接字符集为GBK等Payload:id%bf%27$id接收为%bf%27(URL解码后为两个字节:0xbf和0x27其中0x27是单引号)。str_replace发现单引号0x27将其删除。此时$filtered_id只剩下0xbf。addslashes对0xbf进行处理0xbf不是预定义字符所以原样返回0xbf。$safe_id0xbf。SQL语句:SELECT * FROM users WHERE id‘0xbf‘ AND status1。 失败了单引号在第一步就被删了。思路二利用str_replace删除空字符但addslashes转义单引号的特性结合输入空字符和单引号Payload:id%00%27(即\0‘)$id接收为\0‘(十六进制0027)。str_replace删除单引号‘和空字符\0。那么$filtered_id变成了空字符串“”。addslashes对空字符串处理还是空字符串。SQL语句:SELECT * FROM users WHERE id‘‘ AND status1。 还是失败了所有特殊字符都被删光了。看来在str_replace同时过滤单引号和空字符的情况下直接输入它们俩是行不通的。我们需要让addslashes创造出一个单引号或能导致注入的序列。思路三让addslashes的转义行为被“吃掉”这才是经典宽字节注入的核心。我们需要数据库连接使用GBK、BIG5等宽字符集。假设连接字符集是GBK。 Payload:id%bf%27(再次尝试但这次我们考虑str_replace不删除单引号的情况不题目中会删除)。如果str_replace不删除单引号流程如下$id%bf%27-0xbf27。str_replace发现单引号0x27将其删除。$filtered_id0xbf。addslashes对0xbf无操作。$safe_id0xbf。SQL:... id‘0xbf‘ ...。 无效。我们需要一个Payload使得addslashes在str_replace之后能在某个位置插入一个反斜杠\0x5c并且这个反斜杠能与前面的字符组成一个合法的宽字节字符从而不被当作转义符。假设我们输入id%bf%5c%27(即0xbf5c27)。%5c是反斜杠\。$id0xbf5c27。str_replace删除单引号0x27。$filtered_id0xbf5c。addslashes检查0xbf5c。0x5c是反斜杠是需要转义的字符。所以addslashes会在0x5c前再加一个反斜杠变成0xbf5c5c。$safe_id0xbf5c5c。SQL:... id‘0xbf5c5c‘ ...。 在GBK字符集下0xbf5c可能是一个合法字符如“縗”那么第一个0x5c被“吃掉”第二个0x5c作为字面量反斜杠留在字符串里。但我们的单引号0x27一开始就被删了所以还是无法闭合。看来在str_replace铁了心要删除单引号的情况下直接引入单引号是徒劳的。我们必须换一种思路不使用单引号而是进行数字型注入。5.3 转向数字型注入与布尔盲注如果SQL语句是WHERE id $safe_id而不是WHERE id ‘$safe_id‘那么问题就变成了数字型注入。我们不需要闭合引号只需要注入SQL逻辑即可。但题目通常会用引号包裹确保是字符串比较。不过我们可以尝试通过注入将字符串比较转换为数字比较或者利用类型转换。另一种可能是题目源码中str_replace只过滤了引号但没有过滤反斜杠\和其他字符。那么我们可以尝试输入id1\0 or 11之类的Payload但\0会被过滤。根据公开的CISCN2019 Hack World题解这道题的真实过滤逻辑是$id $_GET[‘id’]; $id str_replace(array(“or”, “and”), ““, $id); // 过滤了or和and关键词大小写 $id addslashes($id); $sql “SELECT * FROM articles WHERE id $id”; // 注意这里没有引号是数字型注入如果是这样那整个攻击面就完全不同了它过滤了or和and关键词通常不区分大小写str_replace是区分大小写的然后进行转义。由于是数字型注入我们不需要处理引号闭合只需要绕过对or和and的过滤即可。绕过str_replace关键词过滤的常用技巧双写绕过因为str_replace是一次性替换且是简单字符串匹配。如果我们将oorr中的or替换为空字符串那么剩下的字符正好又组成了or。 例如 输入id1 oorr 11经过str_replace(“or”, ““, $id)1 oorr 11-1 11(中间的oorr中的or被删除剩下or正好是or) 所以Payload1 oorr 11经过过滤后变成了1 or 11成功绕过了过滤。addslashes在这里的作用是什么它会对空格进行转义吗不会。它只转义单引号、双引号、反斜杠和NULL。所以对于1 oorr 11addslashes不会做任何改变假设没有那些字符。最终SQL为SELECT * FROM articles WHERE id 1 or 11恒真注入成功。布尔盲注的利用在CTF题目中往往不会直接回显数据而是通过页面差异真/假来推断数据这就是布尔盲注。对于Hack World常见的解法就是利用布尔盲注配合双写绕过过滤逐位爆破flag的值。 例如if(ascii(substr((select database()),1,1))100,1,0)这样的表达式结合oorr和anandd来绕过过滤。5.4 最终攻击流程总结针对Hack World题型确定注入点与类型通过id1和id1‘测试发现是数字型注入无引号包裹且存在关键词过滤or/and被过滤。绕过关键词过滤使用双写绕过如oorr,anandd。构造布尔盲注Payload判断数据库名长度id1 anandd (select length(database())5)- 过滤后为1 and (select length(database())5)逐位爆破数据库名id1 anandd ascii(substr((select database()),1,1))100-1 and ascii(substr((select database()),1,1))100通过页面返回的真/假如“Hello”或“Error”来判断条件是否成立。编写自动化脚本使用Python的requests库通过二分法快速逐位爆破表名、列名、字段值。这里addslashes()似乎没有起到防御作用因为输入是数字型没有引号需要转义。那么题目中“addslashes和str_replace的相爱相杀”体现在哪里呢可能体现在另一种过滤场景如果过滤了or和and后又用addslashes转义但用户输入中包含了被转义的字符可能会意外地破坏我们双写绕过的构造例如输入anandd如果其中包含单引号被转义后可能影响字符串匹配但在这个数字型注入场景下不太需要引号。另一种可能是原题过滤了更多内容比如空格、union、select等并且addslashes的存在使得某些利用\0截断的技巧成为可能。但根据主流解法双写绕过是这道题最直接有效的办法。6. 防御之道如何避免此类“相爱相杀”的漏洞通过上面的分析我们可以看到不安全的原因不在于addslashes或str_replace本身而在于错误地组合使用它们以及错误地依赖它们进行安全防护。6.1 根本原因错误的安全感认为多层过滤/转义一定更安全。处理顺序不当不同的处理顺序可能导致完全不同的结果。对函数行为理解不透彻特别是对特殊字符如\0、URL编码字符的处理方式。依赖黑名单str_replace过滤特定关键词是典型的黑名单方式极易被绕过双写、大小写、注释、编码等。错误处理类型对于数字型参数使用字符串转义函数是无效的应该进行类型强制转换。6.2 正确的防御姿势使用参数化查询预编译语句这是防止SQL注入的终极武器。无论是PDO还是MySQLi都支持参数化查询。它将SQL语句的结构与数据分离数据库引擎会严格区分代码和数据从根本上杜绝注入。// PDO 示例 $stmt $pdo-prepare(“SELECT * FROM users WHERE id :id AND status 1”); $stmt-execute([‘:id’ $_GET[‘id’]]); // MySQLi 示例 $stmt $conn-prepare(“SELECT * FROM users WHERE id ? AND status 1”); $stmt-bind_param(“i”, $_GET[‘id’]); // “i” 表示整数类型 $stmt-execute();严格输入验证与类型转换对于数字型参数使用intval()、filter_var($id, FILTER_VALIDATE_INT)进行验证和强制转换。对于字符串参数定义允许的字符白名单如只允许字母数字而不是黑名单。$id $_GET[‘id’]; if (!is_numeric($id)) { die(“Invalid input”); } $id intval($id); // 或者使用白名单 if (!preg_match(‘/^[a-zA-Z0-9_]$/’, $username)) { die(“Invalid username”); }如果需要转义使用数据库专用的转义函数addslashes是通用的但不同数据库的转义规则可能有细微差别。应使用MySQLi:$conn-real_escape_string($string)PDO: 配合参数化查询无需手动转义。 但请注意转义函数不能用于数字型参数且其安全性也依赖于正确的字符集设置。设置正确的数据库连接字符集确保连接字符集与Web应用字符集一致并优先使用utf8mb4避免宽字节注入问题。在连接数据库后立即执行SET NAMES ‘utf8mb4‘。最小权限原则数据库连接用户只赋予必要的最小权限避免使用root或高权限账户。避免动态拼接SQL尽可能使用ORM框架或查询构造器它们通常内置了安全处理。6.3 代码审计时的关注点当你审查代码时看到addslashes和str_replace或类似的字符串替换函数同时出现特别是用于处理用户输入时就应该立即亮起红灯检查处理顺序理解数据流。检查是否用于数字型输入如果是转义是无用的。检查str_replace的黑名单是否完整是否容易被绕过。最终评估是否能用参数化查询重构。7. 总结与延伸思考回顾这道CISCN2019的题目它像是一个精巧的“教学案例”展示了即使是最基础、最常用的函数如果对它们的理解停留在表面组合使用不当也会构筑起脆弱的安全防线。addslashes和str_replace的“相爱相杀”本质上是由于开发者对安全机制的理解偏差和“偷懒”心理——试图用简单的字符串处理来解决复杂的安全问题。这道题也提醒我们在安全攻防中细节决定成败。一个空字符\0一个双写技巧往往就是突破防线的关键。作为开发者必须建立起“默认不安全”的心态对所有用户输入保持警惕并采用业界公认的最佳实践如参数化查询来构建应用。作为安全研究者或CTF选手则需要培养对代码的敏感度能够快速识别这种“特征代码”并熟练运用各种绕过技巧。从这道题出发可以进一步探索其他类似函数的组合会产生什么漏洞如htmlspecialchars与字符串替换在更复杂的字符编码如UTF-8、GB2312环境下过滤和转义会有哪些新问题除了SQL注入这种输入处理缺陷是否可能导致XSS、命令注入等其他漏洞安全是一个持续学习和对抗的过程。每一个漏洞的分析不仅是为了破解一道CTF题更是为了在真实的开发中避免踩入同样的陷阱。希望这次对addslashes和str_replace的深度剖析能让你对PHP安全、乃至Web应用安全有更扎实的理解。下次当你看到类似的代码时相信你一定能一眼看穿其中的玄机。