1. 项目概述:当命令执行遇上字符集限制
在CTF(Capture The Flag)的Web安全挑战中,命令执行(Command Execution)是一个经典且高频的考点。它模拟了攻击者如何利用Web应用的漏洞,在服务器上执行任意系统命令,从而获取敏感信息、读取文件,甚至直接获取服务器控制权(即“getshell”)。然而,出题人为了增加难度,常常会设置各种“过滤”或“限制”,其中一种极具挑战性的情况就是“无数字字母”命令执行。
所谓“无数字字母”,指的是在构造恶意命令时,我们无法直接使用大小写字母(a-z, A-Z)和数字(0-9)。这几乎封死了我们使用常规命令(如ls,cat,whoami)和路径(如/etc/passwd)的可能性。这听起来像是一个不可能完成的任务,但正是这种极端的限制,逼迫我们深入理解操作系统命令解释的原理、编程语言的特性以及数据在计算机中的多种表示形式。解决这类问题,不仅能帮助你在CTF比赛中得分,更能深刻理解安全防护的绕过思路和底层系统的灵活性,对于从事安全研究、渗透测试或代码审计的人来说,是必须掌握的高级技巧。
2. 核心思路拆解:我们能用什么?
当数字和字母被禁用,我们的武器库似乎瞬间清空。但别急,让我们换个角度思考:计算机最终执行的机器码是二进制的0和1,字符只是人类为了方便而赋予的符号。因此,我们的核心思路就是利用非数字字母的字符,通过巧妙的组合和运算,动态地“生成”或“表示”出我们需要的字母和数字。
2.1 可用的字符资源盘点
首先,我们需要明确在Linux的Bash Shell环境下,哪些字符是我们依然可以自由使用的(假设过滤规则仅剔除字母和数字):
- 符号:
$,(,),{,},[,],=,+,-,*,/,%,&,|,!,~,“,‘,\,#,@,?,<,>,.,:,;,,,_。 - 空格:通常用
${IFS}、$IFS$9或<、>来替代,因为空格本身也可能被过滤。 - 引号:单引号
‘和双引号“对于构造字符串至关重要。
在这些字符中,一些具有特殊功能的字符将成为我们构造Payload的基石:
- 变量操作符
$:用于引用变量,如$PATH。更重要的是,它可以用于$(...)命令替换和${...}变量扩展。 - 通配符
*和?:在文件名匹配中,*代表任意字符,?代表一个字符。这在定位文件时极其有用。 - 位运算符
~、&、|:可以用于数字的按位运算。 - 逻辑运算符
!:可用于取反。 - 括号
()和{}:用于定义执行顺序、代码块以及进行各种扩展。
2.2 核心突破原理:异或(XOR)与取反(NOT)
在PHP环境下(CTF Web题很多是PHP),我们有两种强大的武器来生成任意字符串:取反(~)和异或(^)。
原理简述:在PHP中,对一个字符串进行取反(~)操作,是对该字符串中每个字符的ASCII码进行按位取反。例如,字符a的ASCII码是97,二进制为01100001,按位取反后得到10011110,即十进制的-98(在PHP中,对字符取反的结果通常以不可见字符显示,但我们可以用urlencode查看其十六进制形式%9E)。更妙的是,取反操作是可逆的。如果我们对取反后的结果再进行一次取反,就能得到原字符:~~’a’本质上还是’a’。
那么,如果我们无法写出’a’,但可以写出~’a’所对应的那个“乱码”字符呢?在“无数字字母”的限制下,我们虽然不能直接输入a,但可以通过其他方式(比如利用PHP中字符串的自增特性,或从已有变量中切割)得到一个包含我们所需字符的字符串,然后对其整体或部分进行取反,来得到我们想要的命令。
例如,在PHP中,$_=’a’;$a=~$_,那么$a就是~’a’。如果我们能构造出$a,那么~$a就是a。关键在于,我们如何在不使用字母数字的情况下,初始化一个变量并赋值。
一个经典的技巧是利用PHP的可变变量和未初始化变量的类型转换。但更通用、更强大的方法是利用异或运算。任何两个字符进行异或运算,都可能产生第三个字符。如果我们能构造出两个由允许字符组成的字符串,并且它们的异或结果正好是我们想要的命令字符串,那么问题就解决了。通常,我们会先构造出一个全是可见特殊字符的字符串(称为“字典”),然后通过精心计算,找到另一串特殊字符,使得两者异或后得到cat /flag这样的命令。
注意:在实际CTF题目中,出题人可能会禁用
~、^等关键运算符。因此,我们需要掌握多种方法,不能只依赖一种。
2.3 备选方案:自增与通配符
1. Shell中的通配符/???/?at: 在Bash中,如果我们想执行cat命令,但c、a、t都不能写,可以尝试用通配符匹配。系统/bin目录下通常有cat命令。我们可以用/???/?at来匹配它。/???匹配/bin(四个字符),/?at匹配cat(三个字符,首字符任意)。同理,/???/?as可能匹配bash。这常用于执行已知路径下的命令。
2. PHP中的字符串自增: 在PHP中,字符串支持自增操作,行为类似于Perl。例如,$a=’a’; $a++;后$a等于’b’。更神奇的是,$a=’’; $a++的结果是’1’?不对,空字符串自增是’1’,但如果我们从’_’(下划线)或’!’等允许的字符开始,通过多次自增,理论上可以“迭代”出我们想要的字符。例如,‘_’的ASCII码是95,经过特定次数的自增可以变成小写字母。但这种方法通常效率较低,且需要知道起始字符和目标的“距离”。
3. 实战构造:从零到Payload
我们假设一个典型的CTF题目场景:题目提供了一个Web页面,有一个参数(比如cmd)存在命令执行漏洞,但后端对cmd的值进行了严格过滤,移除了所有数字和字母。我们的目标是读取服务器上的/flag文件。
3.1 环境侦察与可用变量
首先,我们需要知道当前环境有哪些“遗产”可以利用。即使在严格过滤下,PHP中依然存在大量预定义变量(超全局变量),其中可能包含字母,但它们是系统定义的,我们的输入无法控制它们的存在。然而,我们可以引用它们。例如,$_、$__、$___……这些单下划线、双下划线变量在默认情况下可能未定义或为空,但它们作为变量名是合法的。更重要的是,我们可以通过${_}的形式来引用它们。
在Linux的Shell中,环境变量是一大宝库。即使命令被过滤,我们依然可以尝试输出环境变量来看看有没有可利用的字符串。在Bash中,${PATH}变量包含字母,但我们无法直接输入PATH。不过,我们可以利用$*、$@、$?等特殊变量,或者通过${PWD:0:1}这种字符串截取方式(如果PWD可用)来提取单个字符。但在无字母数字限制下,PWD、PATH这些变量名本身也含有字母,无法直接写出。
因此,更常见的突破口在PHP层面。我们可以构造一个不含字母数字的PHP代码片段,让它动态生成我们想要的命令字符串。
3.2 基于取反(~)的Payload构造
这是目前最主流和高效的方法。核心步骤是:
- 找到一个可以不使用字母数字初始化的变量,并赋值为一个特殊字符(或空)。
- 通过一系列自增、拼接或取反操作,让这个变量的值变成我们想要的命令字符串的取反形式。
- 对这个变量进行取反操作,得到原始命令字符串。
- 利用
shell_exec、system、passthru等函数(需要绕过函数名过滤)或反引号执行该命令。
一个经典的Payload生成过程如下(假设我们最终要执行cat /flag):
我们首先需要得到字符串cat /flag。我们选择先得到它的取反形式。在PHP中,我们可以写一个小脚本计算出~’cat /flag’:
<?php echo urlencode(~'cat /flag'); // 输出:%9E%8C%8C%9A%8D%8B%9A%92%D7%D8%93%93%97%8D ?>现在,我们需要在不使用字母数字的情况下,构造出这个十六进制字符串所对应的二进制数据。我们可以利用PHP中,两个字符串异或(^)或取反(~)来生成新字符串的特性。但这里我们采用一种更直接的方法:利用.(点号)进行字符串拼接,以及利用$后接非字母字符创建变量。
一个广为流传的Payload构造方法是:
<?php $_=~%9E%8C%8C%9A%8D%8B%9A%92%D7%D8%93%93%97%8D; // 这行在真实攻击中无法直接写,因为包含了% // 实际上,我们需要构造出这个取反后的字符串 ?>更实际的构造是利用一个起始字符,通过自增得到所有必要字符。但自增需要循环,在无字母数字条件下写循环很困难。因此,实战中通常采用提前计算好Payload,然后一次性赋值的方法。这需要利用到PHP的短标签和进制转换。
例如,我们可以通过$_=‘<<<’然后进行一系列操作吗?不,这需要字母。
真正的无数字字母构造往往是从一个空变量或下划线开始,通过嵌套的变量赋值和取反来实现。例如:
<?php $_=[].“”; // 得到一个空字符串,但`[]`和`“”`可能被过滤 ?>一个更可靠的起点是$_=~“”;。在PHP中,对空字符串取反会得到一个特殊的内容。但我们需要一个具体的、可打印的字符作为“种子”。
实际上,最经典的Payload利用了PHP的以下特性:
$_=‘_’;可以得到一个下划线(假设_不被过滤)。- 但是
‘_’包含了字母(下划线被视为字母吗?在正则\w里是,但在简单的字符串比较中,下划线_通常不会被归类为“字母”,它属于“下划线字符”。很多过滤正则/[a-zA-Z0-9]/不会过滤掉_)。 - 如果
_可用,那么$_=‘_’;就初始化了一个变量。然后我们可以用$$(可变变量)等进行复杂操作。
由于构造过程极其复杂,通常我们会使用自动化工具来生成Payload。但理解其原理至关重要。最终生成的Payload可能看起来像一堆天书:
${~%A0%B8%BA%AB}【等等】这串%A0%B8...就是phpinfo等函数名经过取反后URL编码的结果。
3.3 利用通配符和已有命令的Shell Payload
在Shell层面,如果我们能够注入命令,即使不能写字母,也可以利用通配符和系统内建命令。
假设我们可以注入这样的点:?cmd=我们想执行cat /flag。
cat可以用/???/?at替代。- 空格可以用
${IFS}替代。 /flag是路径,无法用通配符,除非flag文件名未知但格式已知(如flag.txt可以用/????.???匹配)。
那么一个可能的Payload是:
?cmd=/???/?at${IFS}/?????这个Payload尝试用/???/?at匹配/bin/cat,用/?????匹配/flag(假设flag是5个字母)。但成功率依赖于系统路径和文件名。
为了更可靠地执行命令,我们可能需要先确定/bin目录下有哪些命令可用。我们可以尝试执行/???/?,这会列出/bin下所有三个字母的命令,但输出可能被过滤或看不到。更高级的技巧是利用重定向,比如将命令结果写入一个web目录下的文件,然后通过web访问该文件。但这需要写权限。
另一个强大的Shell技巧是使用内联命令替换和进制转换。例如,echo $((0x63))会输出99,即字符c的ASCII码。但echo、$((...))都包含了字母。我们可以用printf配合八进制或十六进制。但printf也有字母。
在极端限制下,Shell层面的无数字字母RCE通常需要结合环境变量中的字符串片段。例如,$PATH变量通常包含字符串/bin、/usr等。我们可以通过${PATH:0:1}来截取第一个字符/。但PATH和数字0、1都包含字母数字。不过,我们可以利用特殊变量:$?是上一条命令的退出状态(数字),$$是当前进程ID(数字)。如果我们可以通过某种方式让$?等于一个特定的数字,再结合printf或awk将其转换为字符,就可能构造出字母。例如,$(($?))可能得到0,但如何得到99呢?这非常困难。
因此,在实战中,PHP层面的取反/异或构造法比Shell层面的通配符法更为通用和强大,尤其是在代码执行(如eval($_POST[‘a’]))的场景下。而在系统命令注入(如shell_exec($_GET[‘cmd’]))的场景下,如果过滤发生在PHP代码调用shell_exec之前(即我们的输入先经过PHP函数过滤,再传入shell_exec),那么我们依然需要在PHP层面构造出绕过过滤的字符串。
4. 工具辅助与自动化生成
手动构造无数字字母的Payload是一项繁琐且容易出错的工作。幸运的是,安全社区已经开发出一些优秀的工具来帮助我们自动化这个过程。
4.1 使用工具生成PHP取反Payload
最著名的工具之一是p牛(phith0n)编写的脚本,通常集成在CTF工具集或AWD/CTF相关的工具箱中。其核心原理是:
- 指定你想要执行的命令或代码,例如
cat /flag。 - 脚本会自动计算该命令字符串的取反(
~)后的URL编码形式。 - 然后,脚本会生成一段PHP代码,这段代码仅使用非字母数字字符,通过变量操作、字符串拼接和最终的取反操作,还原并执行原命令。
生成的Payload通常长这个样子:
<?=$_=‘?’^‘E’;$_=‘?’^‘E’;$__=‘?’^‘-’;$___=‘?’^‘.’;${$_}[$__](${$_}[$___]);?>这看起来像乱码,但实际上是利用了异或运算。‘?’^‘E’的结果是字符t(因为?的ASCII是63,E是69,63^69=106,即j… 这里只是举例,实际计算需精确)。这段代码通常用于连接一句话木马。
对于取反构造,工具生成的Payload可能更晦涩,如:
${~%A0%B8%BA%AB}{%A0}(${~%A0%B8%BA%AB}{%A1});其中%A0%B8%BA%AB是_POST的取反URL编码,%A0和%A1分别是键名。
使用方法:
- 在本地PHP环境或确信安全的沙箱中运行生成脚本。
- 输入你想要执行的命令。
- 复制输出的Payload,提交到目标题目。
实操心得:不要完全依赖工具。理解工具生成的Payload每一部分的含义至关重要。因为题目环境可能微有不同(如PHP版本、禁用函数、魔术引号等),可能导致工具生成的Payload失效。届时你需要手动调整。例如,如果
$符号也被过滤,那么整个思路都需要改变。
4.2 利用在线平台或浏览器Console进行计算
如果你没有现成的工具,也可以利用在线的PHP代码执行环境或浏览器开发者工具的Console进行辅助计算。
步骤:
- 打开一个在线的PHP代码执行网站(确保仅用于测试)。
- 编写计算取反或异或的代码,例如:
<?php $cmd = ‘cat /flag’; $neg = ~$cmd; for($i=0; $i<strlen($neg); $i++){ echo ‘%’.dechex(ord($neg[$i])+256); } ?> - 得到取反后的编码串。
- 然后,你需要构思如何用非字母数字字符构造出这个编码串对应的变量。这可能涉及到用
“”(空字符串)的自增特性,或者从$_SERVER[‘QUERY_STRING’]等超全局变量中提取特定字节。
4.3 手工构造的思维训练
尽管有工具,但手工构造的能力依然重要。以下是一个简化的思维训练:
目标:在不使用字母数字的情况下,让PHP输出字符串“a”。
思路:
- 我们需要一个起始点。PHP中,未定义的常量会被当作字符串使用,并产生一个Notice。例如,
_(一个下划线)是一个合法的常量名吗?不,它需要被定义。但我们可以用“_”字符串。 - 假设我们可以使用下划线
_和点号.。我们可以尝试$_=‘_’;。但‘_’包含了引号,引号是允许的。 - 现在
$_的值是‘_’。字符‘_’的ASCII是95。‘a’的ASCII是97。我们需要将95变成97。 - 我们可以用自增:
$_++;执行一次后,$_变成‘‘‘(96对应的字符,反引号)。再执行一次$_++;,就变成了‘a’。 - 所以,最终代码可以是:
$_=‘_’;$_++;$_++;echo $_; - 检查:
_、+、=、;、echo中的e、c、h、o都是字母!echo不能用。 - 我们需要用其他方式输出,比如用
<?=$_?>短标签。但<?=中的?和=是允许的。前提是短标签开启。 - 所以,最终可行的Payload可能是:
<?=$_=‘_’;$_++;$_++;$_?> - 这里仍然有
$_和‘_’。$_变量名是允许的($加下划线)。‘_’字符串是允许的。自增运算符++是允许的。短标签<?= ... ?>是允许的。成功!
这个例子展示了手工构造的基本逻辑:从允许的字符集合中找到一个“种子”,通过允许的操作(自增、拼接、位运算)将其“培育”成目标字符。
5. 高级技巧与组合绕过
当简单的取反和异或被过滤时,我们需要组合更多技巧。
5.1 利用PHP字符串解析特性
PHP在将参数传递到函数时,会对字符串进行一些解析。例如,$_GET[‘a’],如果传入a[]=1,那么$_GET[‘a’]就是一个数组。我们可以利用这个特性来传递一些特殊值。
在无数字字母的上下文中,我们可以考虑用非字母数字的字符作为数组键名。例如,如果我们能控制一个变量名,我们可以用${‘_’}的形式来引用变量$_。更进一步,我们可以用${“\x61”}来引用变量$a,因为\x61是a的十六进制。但这里出现了字母x。我们可以用${~%8C%86}这样的形式吗?这又回到了取反。
5.2 利用.iconv编码转换与异常
一个非常有趣的技巧是利用PHP的iconv扩展和非法字符序列。例如,iconv(“UCS-4LE”, “UCS-4BE”, ‘xxxx’)可能会产生意想不到的结果。在某些PHP版本下,通过精心构造的编码转换,可以触发异常并在错误信息中泄露或执行代码。但这需要特定的PHP环境配置,并非通用解法。
5.3 利用.htaccess与文件上传
如果题目存在文件上传点,并且允许上传.htaccess文件,我们可以尝试绕过。.htaccess中可以设置php_value来修改PHP配置。我们可以尝试用非字母数字的编码来设置auto_prepend_file为某个我们上传的图片马。但如何用非字母数字写出.htaccess文件的内容呢?如果上传过程本身没有字符过滤,我们可以直接上传一个包含正常字母数字的.htaccess。这里的“无数字字母”限制可能仅针对某个特定的输入点(如命令执行参数),而不是整个应用。因此,识别限制的范围非常重要。
5.4 无参数RCE与函数链
这是“无数字字母”挑战的一个变种:不仅不能有数字字母,还不能有任何参数(即函数调用必须是a(b(c()))这种形式,不能是a(‘xxx’))。这就需要我们利用PHP内置函数的返回值作为其他函数的参数,形成一个调用链。例如:
localeconv()返回包含小数点.的数组。current(localeconv())得到.。chr(ord(‘.’))等等,最终可以构造出文件名或命令。- 结合
scandir()、readfile()、getcwd()等函数,实现目录遍历和文件读取。
在无数字字母且无参数的情况下,通常需要先找到一个返回特定字符(如点号.)的函数,然后以此为基础,通过chr()、ord()、next()、array_reverse()等函数进行变换,最终拼凑出flag、cat等关键字。这更像是一场“函数编程”游戏。
6. 防御视角与总结
从防御者角度看,“无数字字母”命令执行挑战揭示了黑名单过滤的局限性。仅仅过滤字母和数字是远远不够的,因为编程语言和Shell环境的灵活性超乎想象。
有效的防御措施应包括:
- 白名单替代黑名单:严格定义允许的字符集,只放行必要的字符(如对于文件名参数,只允许字母、数字、点、下划线、连字符)。
- 最小权限原则:运行Web服务的用户权限应尽可能低,避免其执行高危命令或访问敏感文件。
- 禁用危险函数:在PHP中,在
php.ini中禁用system、shell_exec、exec、passthru、proc_open、popen等函数。 - 使用安全的API:对于需要执行系统命令的场景,尽量使用语言提供的、参数化的安全API(如Python的
subprocess.run([‘ls’, ‘-la’])),避免直接拼接字符串。 - 输入验证与转义:对所有用户输入进行严格的验证和转义,特别是在拼接进命令或SQL语句之前。
- 部署WAF:Web应用防火墙可以帮助拦截一些常见的攻击Payload。
对于安全研究者而言,攻克“无数字字母”这类题目,是对思维深度和知识广度的一次绝佳锻炼。它要求你不仅知道漏洞怎么利用,更要理解计算机系统如何解释和执行命令,理解数据在各种形式之间的转换。每一次成功的绕过,都是对系统底层原理的一次致敬。在实际的渗透测试中,这种极端的绕过技巧可能不常用,但其中蕴含的“跳出框框思考”的思维模式,是每一个安全从业者宝贵的财富。我个人的体会是,在遇到这种题目时,不要急于寻找现成的Payload,而是静下心来,从允许的字符出发,像搭积木一样,一步步推理出可能的构造路径,这个过程本身带来的成就感,远比直接拿到flag要大得多。