1. 项目概述从一道CTF题看PHP字符串函数的攻防实战最近在带新人刷攻防世界的Web进阶区遇到了一道挺有意思的题目叫“web2”。这道题本身难度不算顶级但它非常经典地展示了PHP中两个看似简单的字符串处理函数——strrev和str_rot13——在CTF逆向场景下的应用。很多刚接触Web安全逆向的朋友看到页面上一串乱码似的字符串或者经过简单变换的密文往往会感到无从下手。这道题就是一个绝佳的入门案例它没有复杂的加密算法没有晦涩的编码核心就是考察你对PHP内置函数特性的理解以及逆向思维的建立。简单来说这道题会给你一个经过strrev字符串反转和str_rot13ROT13编码处理后的“flag”字符串。你的任务就是逆向这个过程还原出原始的明文flag。这听起来很简单对吧但实战中新手常犯两个错误一是看到密文就盲目尝试各种在线解密工具结果徒劳无功二是知道要用这两个函数但搞反了顺序导致结果错误。这道题的价值就在于它强迫你停下来理解数据流动的“方向”这正是逆向工程的核心——从结果倒推过程。接下来我会带你完整拆解这道题。我们不仅会还原解题步骤更重要的是我会分享如何系统性地分析这类字符串变换题目如何搭建本地测试环境快速验证猜想以及如何将这种思路迁移到更复杂的实战场景中。无论你是正在入门CTF的Web方向还是想巩固PHP安全知识这篇实战笔记都能给你带来直接的帮助。2. 核心原理拆解strrev与str_rot13的运作机制要逆向一个过程首先必须彻底理解正向过程是如何发生的。strrev和str_rot13都是PHP中用于字符串变换的内置函数它们本身并非为加密设计但在CTF中常被用作简单的“混淆”或“编码”手段。2.1 strrev简单的逆序排列strrev函数的功能极其单纯将输入的字符串完全反转。echo strrev(Hello World); // 输出dlroW olleH echo strrev(12345); // 输出54321 echo strrev(flag{abc}); // 输出}cba{galF它的工作原理是线性遍历字符串从最后一个字符开始依次将字符放入一个新字符串的开头直到第一个字符。这个过程没有密钥没有复杂的算法就是纯粹的顺序翻转。在逆向时这意味着对经过strrev处理的结果再次应用strrev就能得到原始字符串。这是一个对合操作或者说它是自身的逆函数。注意strrev对多字节字符如中文的处理会出问题因为它按字节反转可能破坏UTF-8等编码的字符结构。但在CTF的flag中通常只包含ASCII字符字母、数字、标点所以这个问题一般不会遇到。2.2 str_rot13经典的凯撒移位变种str_rot13是ROT13旋转13位编码的PHP实现。ROT13是凯撒密码的一种偏移量固定为13。规则对字母表中的每个字母将其向后移动13位。如果移动后超出字母表范围则绕回开头。具体操作大写字母A-ZASCII 65-90ROT13(chr) chr((ord(chr) - 65 13) % 26 65)小写字母a-zASCII 97-122ROT13(chr) chr((ord(chr) - 97 13) % 26 97)非字母字符如数字、花括号{}、下划线_保持不变。echo str_rot13(Hello World); // 输出Uryyb Jbeyq echo str_rot13(abc123); // 输出nop123 echo str_rot13(flag{test}); // 输出synt{grfg}ROT13有一个非常关键的特性它也是自身的逆函数。因为字母表有26个字母移动13位后再移动13位正好绕回原位。即str_rot13(str_rot13($text)) $text。2.3 组合变换与逆向逻辑当strrev和str_rot13组合使用时正向过程可以看作一个函数链$ciphertext str_rot13(strrev($plaintext))。当然出题人也可能先做ROT13再做反转即strrev(str_rot13($plaintext))。这两种顺序的结果截然不同。逆向解密过程就是正向过程的逆序执行且每个步骤使用对应函数的逆操作。由于这两个函数都是自逆的所以如果正向是str_rot13(strrev($plaintext))则逆向为strrev(str_rot13($ciphertext))。如果正向是strrev(str_rot13($plaintext))则逆向为str_rot13(strrev($ciphertext))。这里就是第一个易错点你必须判断出题人采用的组合顺序。如何判断通常有两种方法观察密文字符特征先对密文尝试str_rot13看结果是否更像一个反转后的字符串例如可能出现}...{的结构这是反转后flag的常见特征。或者先尝试strrev看结果是否像一段ROT13编码字母被替换但数字和括号不变。本地模拟与快速验证这是最可靠的方法。根据题目描述或上下文猜测flag格式如flag{...}、ctf{...}用PHP写一个简单的脚本枚举两种顺序看哪种能得出符合格式的结果。理解了这个原理我们就掌握了破解此类题目的钥匙。接下来我们进入实战环节看看在攻防世界的“web2”题目中如何具体应用。3. 实战环境搭建与题目分析在真正解题前我强烈建议你搭建一个本地PHP测试环境。这不仅能让你安全、快速地验证思路更是Web安全学习者的必备习惯。盲目在线上题目环境里“试错”效率很低而且有些操作可能受限。3.1 快速搭建本地PHP测试环境对于这种单文件字符串操作最简单的环境就是使用PHP的命令行接口CLI。安装PHP如果你还没有PHP去官网下载对应操作系统的版本安装即可。确保PHP可执行文件路径已添加到系统环境变量。验证安装打开终端Windows是CMD或PowerShellMac/Linux是Terminal输入php -v能看到版本信息即说明安装成功。创建测试脚本新建一个文本文件命名为solve_web2.php。我们将在这里编写解题代码。为什么要用命令行因为它轻量、快捷无需配置复杂的Web服务器如Apache/Nginx特别适合这种逻辑简单的本地计算任务。当然你也可以用XAMPP、WAMP等集成环境或者甚至用在线的PHP代码沙盒但本地CLI是最可控、最直接的方式。3.2 题目“web2”场景还原与初步观察根据记忆和常见题型攻防世界“web2”题目通常会呈现类似如下的场景 你访问题目链接后网页上可能会直接显示一段密文也可能需要你查看网页源代码CtrlU或者在某个输入框、JavaScript代码中找到一段经过处理的字符串。常见的密文可能长这样synt{grfg}或}tqrg{nys之类的变体。第一步永远是信息收集查看网页源代码按F12打开开发者工具查看Elements和Console标签页有时flag或提示会藏在注释或JS变量里。分析字符串特征拿到疑似密文的字符串后仔细观察。是否包含数字、花括号{}这些字符在ROT13下是不变的。字符串的开头或结尾是否有}或{这强烈提示可能经过strrev反转因为正常flag是flag{...}反转后会变成}...{galf。字母部分是否看起来像经过替换例如看到“synt”可能对应“flag”f-s, l-y, a-n, g-t这符合ROT13的映射关系。假设我们在题目中找到了如下密文}tqrg{nys。我们来做初步分析它以}开头以s结尾。正常flag以f开头}结尾。这很像一个反转后的字符串。中间的nys部分如果应用ROT13解码因为ROT13自逆所以编码和解码是同一个操作n-a, y-l, s-f得到alf。这看起来像是“laf”或“alf”结合开头}...{galf反转后正是flag{...}。这给了我们很强的信心猜测原始操作可能是先对明文flag做strrev然后再做str_rot13。有了假设接下来就是用代码验证。4. 逆向解题步骤与代码实现分析之后我们进入具体的解题环节。我将演示两种方法一种是直接在PHP交互环境或脚本中分步计算另一种是编写一个健壮的解题脚本。4.1 方法一分步手动计算理解过程假设密文$cipher “}tqrg{nys”;我们的假设正向过程是$cipher str_rot13(strrev($flag))那么逆向过程就是$flag strrev(str_rot13($cipher))我们来手动推演对密文}tqrg{nys应用str_rot13}-}(非字母不变)t-g(t是字母t(116)属于小写字母a-z (116-9713)%2697 (1913)%2697 32%2697 697103 - ‘g’)q-d(q(113): (113-9713)%2697 (1613)%2697 29%2697 397100 - ‘d’)r-e(r(114): (114-9713)%2697 (1713)%2697 30%2697 497101 - ‘e’)g-t(g(103): (103-9713)%2697 (613)%2697 19%2697 1997116 - ‘t’){-{(非字母不变)n-a(n(110): (110-9713)%2697 (1313)%2697 26%2697 09797 - ‘a’)y-l(y(121): (121-9713)%2697 (2413)%2697 37%2697 1197108 - ‘l’)s-f(s(115): (115-9713)%2697 (1813)%2697 31%2697 597102 - ‘f’)得到中间结果}gdet{alf对中间结果}gdet{alf应用strrev反转后得到flag{tedg}看我们得到了一个符合格式的字符串flag{tedg}这很可能就是最终的flag。如果题目设计的flag不是这个那也可能是另一种顺序。我们可以用代码快速验证另一种顺序。4.2 方法二编写通用解题脚本手动计算容易出错特别是字符串较长时。编写脚本是更专业、更可靠的做法。下面是一个PHP解题脚本?php // 攻防世界 web2 解题脚本 // 假设从题目获取的密文 $ciphertext “}tqrg{nys”; // 请替换为实际密文 echo “原始密文” . $ciphertext . “\n\n”; // 假设1正向为 str_rot13(strrev($flag)) echo “假设正向顺序str_rot13(strrev(flag))\n”; $step1 str_rot13($ciphertext); echo “第一步str_rot13解密” . $step1 . “\n”; $plaintext1 strrev($step1); echo “第二步strrev反转” . $plaintext1 . “\n”; echo “解密结果1” . $plaintext1 . “\n\n”; // 假设2正向为 strrev(str_rot13($flag)) echo “假设正向顺序strrev(str_rot13(flag))\n”; $step2 strrev($ciphertext); echo “第一步strrev反转” . $step2 . “\n”; $plaintext2 str_rot13($step2); echo “第二步str_rot13解密” . $plaintext2 . “\n”; echo “解密结果2” . $plaintext2 . “\n\n”; // 自动判断哪个结果更可能是flag基于常见格式 function looks_like_flag($str) { // 常见flag格式以 flag{、ctf{、cyber{ 等开头以 } 结尾 return preg_match(/^(flag|ctf|cyber)\{.*\}$/, $str); } echo “有效性判断\n”; if (looks_like_flag($plaintext1)) { echo “结果1 ‘{$plaintext1}’ 符合常见flag格式很可能是正确答案。\n”; } if (looks_like_flag($plaintext2)) { echo “结果2 ‘{$plaintext2}’ 符合常见flag格式很可能是正确答案。\n”; } if (!looks_like_flag($plaintext1) !looks_like_flag($plaintext2)) { echo “两个结果均不符合常见flag格式请检查密文是否正确或考虑其他变换。\n”; } ?将上述代码保存为solve.php在终端运行php solve.php脚本会自动输出两种顺序的逆向结果并尝试根据正则表达式匹配常见flag格式给出提示。实操心得在真实CTF比赛中flag格式可能多变。除了flag{还可能是ctf{、hctf{、cyber{或者没有花括号只有一串哈希值。因此脚本中的正则判断只是一个辅助。最根本的还是理解原理然后人工审查输出结果看哪个看起来像是有意义的单词或约定俗成的格式。5. 深度扩展在复杂场景下的应用与变种掌握了strrev和str_rot13的基础逆向我们就可以应对更复杂一些的变种了。CTF出题人不会总是出这么直白的题他们可能会把这些简单函数嵌套在更复杂的流程里。5.1 多层嵌套与多次变换题目可能不止应用一次变换而是多次、交替使用。例如正向$cipher str_rot13(strrev(str_rot13($plain)))。逆向由于每个函数都是自逆的所以只需完全反向执行即可$plain str_rot13(strrev(str_rot13($cipher)))。编写通用脚本的思路是将正向变换序列保存为一个数组逆向时将数组反转并依次应用对应的函数因为都是自逆的所以函数本身不变。?php // 假设已知正向变换序列 $forward_ops [‘str_rot13’ ‘strrev’ ‘str_rot13’]; $cipher ‘经过三次变换后的密文’; $plain $cipher; // 逆向倒序执行正向操作 foreach (array_reverse($forward_ops) as $op) { $plain $op($plain); } echo $plain; ?5.2 与其他编码/加密结合这是更常见的进阶考法。strrev和str_rot13可能只是整个加密链条中的一环。与Base64结合例如先Base64编码再反转再ROT13。逆向时就需要先ROT13再反转再Base64解码。与十六进制bin2hex结合PHP的bin2hex将字符串转为十六进制表示hex2bin是其逆操作。如果组合进来你需要判断十六进制字符串的特征仅包含0-9, a-f。与自定义替换结合出题人可能自己写一个简单的替换函数。这时就需要你分析密文和可能明文之间的字符映射关系推导出替换表。应对策略识别特征Base64编码结尾常有字符集是A-Za-z0-9/。十六进制字符串长度是偶数且只有0-9a-f。ROT13只影响字母。试探性解码从密文开始尝试最常见的解码操作Base64 decode, hex2bin, URL decode观察结果是否变得更“像”一段可读文本或另一种编码。动态调试如果题目提供了PHP源代码片段可以在本地用不同输入测试观察输出变化从而反推逻辑。5.3 在真实代码审计中的应用在真实的Web应用代码审计中你很少会看到如此直白的strrev(str_rot13($input))。但这类字符串处理函数的身影无处不在。弱比较与类型混淆md5(strrev($password))与数据库存储值比较。如果比较是弱类型可能引发哈希碰撞漏洞。简单的访问控制混淆使用str_rot13来“隐藏”某个关键参数名或路径但本质上只是透明编码毫无安全性可言。自定义解密函数程序员可能自己实现一个“加密”函数里面就用到了反转、移位等操作。审计时需要逆向这个函数。理解这些基本函数的特性能帮助你在代码审计时快速识别出那些“自以为安全”实则脆弱的处理逻辑。6. 常见问题排查与避坑指南即使原理清晰实战中还是会踩坑。下面是我和学员们遇到过的典型问题及解决方案。6.1 问题一执行解密后得到乱码或非预期结果可能原因及排查步骤密文输入错误最常见的问题。网页上的密文可能含有不可见字符如空格、换行、制表符。在复制时务必使用纯文本模式粘贴到你的脚本中或者用trim()函数处理一下。顺序判断错误你假设的正向顺序与实际不符。解决方案务必用脚本枚举两种顺序str_rot13(strrev($c))和strrev(str_rot13($c))并人工检查两个输出。哪个输出包含可读的单词或符合预期的格式如flag{哪个就是正确的。存在其他编码题目可能混合了URL编码%7B对应{、HTML实体#x66;对应f等。在解密前先用urldecode()或html_entity_decode()处理一下密文。函数使用错误在极少数情况下出题人可能使用了自定义的“rot13”或“reverse”函数而非PHP内置函数。这就需要你审计给出的源代码如果有。6.2 问题二在Web题目中如何获取准确的密文查看页面源代码这是第一步。密文可能在HTML注释!-- --里也可能在某个div的value属性或>function rot13(str) { return str.replace(/[a-z]/gi, c String.fromCharCode(c.charCodeAt(0) (c.toLowerCase() ‘n’ ? 13 : -13))); } console.log(rot13(“synt{grfg}”));复制复杂文本使用copy(document.body.innerText)可以将页面所有文本复制到剪贴板方便粘贴到本地编辑器处理。7.3 构建个人解题脚本库这是最有价值的长期投资。将这次解题的PHP脚本保存下来进行泛化。例如创建一个ctf_tools.php文件里面包含各种常用函数function solve_rev_rot13($cipher, $order‘rs’) { // ‘rs’ for rot13 then strrev $ops []; if ($order ‘rs’) { $ops [‘str_rot13’ ‘strrev’]; } else { $ops [‘strrev’ ‘str_rot13’]; } $result $cipher; foreach ($ops as $op) { $result $op($result); } return $result; } function auto_detect_rev_rot13($cipher) { $r1 solve_rev_rot13($cipher, ‘rs’); $r2 solve_rev_rot13($cipher, ‘sr’); return [‘rot13_then_strrev’ $r1, ‘strrev_then_rot13’ $r2]; }随着你遇到的题目类型增多这个工具库会越来越强大成为你解题的“外挂”。回过头看“web2”这道题它就像一把钥匙帮你打开了PHP字符串函数逆向的大门。核心思路永远不变确定变换流程然后逆序执行逆操作。strrev和str_rot13的“自逆”特性让这道题变得简单但更重要的是通过它培养的逆向思维和本地测试习惯。下次当你遇到更复杂的组合编码题时不妨先静下心来像解这道题一样一步步拆解、假设、验证你会发现很多看似复杂的题目都是由这些简单的基石构建而成的。