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:反序列化我会当成单独的一篇去写,莫着急)。
鸣谢: