GitHub: https://github.com/LetheSec/VulnCMS

1 开发过程

1.1 需求分析

基于Linux系统,使用Docker容器,并从PHP、Java、Python中选择一门语言并应用开发框架进行开发。所开发的Web应用系统需内置典型的Web漏洞,其中必须包含:

  • SQL注入

  • XSS

  • 文件上传

  • 文件包含

  • 命令执行

  • XXE

  • 反序列化

部署方式使用Docker容器进行一键部署,方便环境的迁移与搭建,并需要完成对内置典型漏洞的攻击过程。最后还需要对漏洞页面进行修复,并进行验证。

根据需求,进行如下分析:

(1)目前市面上使用PHP进行开发的Web应用系统占据很大一部分,并且国人开发的PHP开发框架ThinkPHP上手简单、文档详细,也被使用的越来越广泛。

(2)除此之外,使用PHP语言来进行内置漏洞的开发时,可以最大程度上覆盖所有类型的漏洞,实现代码也较为经典。而如果使用Java或者Python语言,由于语言本身的特性,对于某些漏洞可能会比较难以实现。

(3)本次Web应用系统包含Web服务、数据库服务等不止一种服务,在使用Docker进行部署时,可以使用Docker Compose,避免对每一个微服务单独编写dockerfile和进行docker build操作。

1.2 系统架构

由需求分析可知,我们需要选择PHP以及ThinkPHP进行本次课程设计的开发,因此开发工具选取JetBrains旗下的PHPStorm作为IDE,基于ThinkPHP5在YXJCMS的基础上进行开发。

网站架构如下:

(1)整个网站分为前台展示、用户后台和管理员后台,前台功能由首页、新闻中心、关于我们、加入我们四个模块。

(2)用户可使用前台的注册、登录功能进入到用户管理界面,在用户管理界面除了可以对个人信息进行修改,还具有在线投稿功能,向管理员发送稿件,经审核可以发表在网站前台。

(3)管理员后台具有系统配置、权限管理、内容管理、内容设置、会员管理几大功能模块,用来对整个Web应用服务进行统一的管理与设置。

1.3 漏洞设计

由于本Web站点具有实际的使用功能,因此考虑将各经典漏洞尽量融合于实际功能点,在满足正常需求的情况下,设计所要求的漏洞,思路如下:

(1)文件包含漏洞

在“关于我们”界面为了引入其他php文件中的信息使用了文件包含操作,直接通过GET传参的方式由file参数传入要包含的文件名,但是由于没有限制包含的文件名,从而可以使攻击者任意包含文件。

(2)SSRF

需求在于关于我们页面中需要引入作者博客页面,因此使用curl语句对本站点以外的web服务发起了请求,同样是为对GET请求中的url参数进行严格过滤,导致攻击者可以任意传入url参数进行SSRF攻击,可以造成任意文件读取、内网端口扫描等危害。

(3)XXE

我们知道实际上doc文件内含了许多xml文件,因此同样可以造成XXE文件。在本系统中,该漏洞存在于“简历上传”功能处,用户可以上传doc文件,而系统在未禁用外部实体的情况下对xml文档进行解析,从而造成XXE漏洞,攻击者可读取任意文件。

(4)弱口令

在渗透测试的过程中,弱口令是非常常见的漏洞之一,且其利用简单,危害大,因为往往管理员后台都会有一些敏感功能。因此在本系统中,管理员后台使用admin/admin的弱口令。

(5)SQL注入

SQL注入漏洞通常存在于对数据库的查询中,本系统的SQL注入漏洞位于用户登陆后,在查看稿件的功能处,存在对数据库的查询操作,其id参数未进行任何过滤,导致可以进行SQL注入。

(6)文件上传漏洞

本系统的文件上传漏洞为了更贴近真实,不存在直接漏洞,而是配合其他漏洞手段先获取管理员后台,而在管理员后台可以设置运行上传的文件后缀,这样就可以造成任意文件上传漏洞了,并且可以直接getshell,思路是比较真实的,危害也是比较大的。

(7)存储型XSS

存储型XSS危害较大,但是一般需要管理员进行交互,让管理员能够看到攻击者构造的XSS
Payload。本系统的XSS漏洞位于用户后台的“在线投稿”功能,通过在稿件中构造Payload并投稿,可以在管理员审核稿件时触发,从而获取管理员账号的Cookie等信息。

(8)CSRF

本系统的CSRF漏洞存在于管理员后台,在系统管理员使用“添加管理员”功能时,提交的表单没有进行Token的验证,这样就会时攻击者能够构造CSRF表单,然后通过诱导管理员点击的方式,任意添加管理员账号。

(9)命令注入

命令注入漏洞位于管理员后台的服务器状态功能,这里可以输入IP地址,如127.0.0.1,提交后会对该IP进行Ping命令并返回结果,但是由于没有进行任何过滤,所以攻击者可以通过拼接闭合任意执行命令。

(10)反序列化

由于反序列化漏洞不好直接结合到此次站点的功能中,所以我在管理员后台中单独添加了一个演示页面,可以对传入的payload进行反序列化,而未进行任何检查,从而造成反序列化漏洞。

1.4 页面展示

(1)前台首页

(2)用户后台

(3)管理员后台

2 漏洞证明

2.1 文件包含漏洞

2.1.1 漏洞复现

文件包含漏洞位于首页的关于我们页面,可以看到url中出现file=about.php参数,即在该页面中包含了about.php:

我们尝试进行文件包含攻击,构造payload:?file=/etc/passwd

可以看到当我们将包含的文件名改为/etc/passwd后,原来的信息不能正常显示了,但是成功回显了/etc/passwd的文件内容,也就意味着包含了该文件内容。

如果想包含PHP文件并能看到文件中的内容的话,就不能直接包含文件名,而是需要利用PHP伪协议。如我们要读取about.php的内容,可以构造payload如下:?file=php://filter/convert.base64-encode/resource=about.php

提交payload后回显了一段base64,我们进行base64解码后得到about.php的内容:

2.1.2 代码分析

漏洞代码位于MyCMS\src\application\about\controller\Index.php的index函数中:

可以看到使用include语句进行文件包含时,没有进行任何的过滤与限制,导致$file变量可控,攻击者可以通过传入该变量的值任意包含文件。

2.2 SSRF

2.2.1 漏洞复现

在关于我们页面中点击“作者博客”,可以看到链接中加入了url参数,如下:?url=https://lethe.site/about/,并且显示了该网址的内容。

可以初步判断存在SSRF,即服务端会请求url中传递的链接。但是攻击者在利用时除了http协议,可以使用file协议读取服务器上的任意文件,如使用如下payload:?url=file:///etc/passwd

可以看到成功通过SSRF漏洞读取了/etc/passwd的内容。

2.2.2 代码分析

漏洞代码位于MyCMS\src\application\about\controller\Index.php的blog函数中:

url参数没有进行任何的过滤与限制操作,直接传入了curl_exec()中进行执行,导致了$url对攻击者可控,攻击者可以通过http、file、gopher等协议进行SSRF攻击,造成任意文件读取、内网端口扫描等攻击。

2.3 XXE

2.3.1 漏洞复现

XXE漏洞存在于首页加入我们页面中的提交简历功能:

这里上传的简历文件限制只能为docx格式,但是通过测试可以发现服务端对会读取docx文件的docProps目录下的core.xml读取xml,所以把docx文件解压后在core.xml里构造payload:

然后在使用zip命令压缩为docx文件:

然后上传我们构造的xxe.docx文件到服务端:

服务器成功解析我们构造的payload并读取了/etc/passwd文件。

2.3.2 代码分析

漏洞代码位于MyCMS\src\application\joinus\controller\Index.php中:

代码中接收上传的文件后,会对docx进行解析,会读取docPorps/core.xml中的内容,并且把//dc:title回显,如果解析过程中出错,则判断不是一个docx文件,但是代码中没有进行任何过滤,也没有对外部实体的引用进行限制,这就导致了攻击者可以构造恶意docx文件从而进行XXE攻击。

2.4 弱口令

2.4.1 漏洞复现

弱口令是真实环境下最常见的漏洞之一,因此在本Web应用系统的管理员后台处设置了弱口令,即用户名和密码均为admin,攻击者可以通过手工测试或者爆破的方式获取用户名和密码,从而登录后台:

成功登陆后台:

2.5 存储型XSS

2.5.1 漏洞复现

存储型XSS需要进行用户登录,在用户管理界面中有在线投稿功能,管理员会对稿件的内容进行审核,因此我们可以在标题处插入XSS的payload如:<script>alert(‘xss’)</script>,然后进行投稿:

提交后等待管理员审核即可。

然后等待管理员对稿件进行审核,即可触发xss如下:

攻击者通过构造payload,可以利用存储型xss获取管理员用户的cookie,从而以管理员的身份进行登录,危害较大。

2.5.2 代码分析

漏洞位于 YXJCMS\application\member\controller\Content.php的publish()函数:

将用户提交上来的投稿表单,没有进行任何的过滤与转义,就直接存入数据库中了,这样导致管理员在审核稿件的时候直接能够看到原始的xss语句,从而触发payload。

2.6 SQL注入

2.6.1 漏洞复现

漏洞位于用户后台的“我的稿件”功能,点击下图的“编辑”:

可以看到之前提交的稿件,并且url中出现了id=2的参数:

我们先通过payload:?id=1 and sleep(5)%23,发现页面延时了5秒,判断存在SQL注入:

直接使用SQLMAP自动化工具进行注入(需要加上cookie):

成功注出结果:

2.6.2 代码分析

漏洞代码位于YXJCMS\application\member\controller\Content.php 的edit函数中:

在进行数据库操作的时候,直接使用了sql语句拼接的方式将参数$id拼接了进去,这样攻击者就可以通过构造$id参数来闭合前面的sql语句,而加上自己的恶意语句,从而造成SQL注入漏洞。

2.7 文件上传漏洞

2.7.1 漏洞复现

本系统中的文件上传漏洞,不能直接利用,需要先用XSS、SQL注入、CSRF等其他漏洞获取管理员后台登录权限,然后修改运行上传的后缀(加上php):

然后在用户后台的“头像上传”功能处进行上传,上传时还要采取一定的绕过方式,即Content-type为:image/png,并在文件前面加上GIF89a:

成功上传了php文件,访问文件路径看到shell.php被成功执行:

这里使用phpinfo进行测试,实际上攻击者可以通过上传木马文件从而直接获得服务器的控制权。

2.7.2 代码分析

漏洞代码位于YXJCMS\application\attachment\controller\Upload.php:

可以看到实际上是对上传的文件进行了过滤的,但是存在一定的逻辑漏洞,即若攻击者能够登录管理员后台,就可以在“运行上传文件”处添加上本身不允许的类型(如php)从而造成漏洞,防御时应该采取任何情况下都不能上传php文件的方式,即使添加了运行项,也无法上传成功。

2.8 CSRF

2.8.1 漏洞复现

CSRF漏洞位于管理员后台的“添加管理员”功能,点击添加管理员并抓包:

然后利用BurpSuite生成CSRF POC:

在这里插入图片描述

POC如下:

然后诱使管理员点击我们构造的页面:

在这里插入图片描述

再次查看可以看到已经成功添加了我们构造的管理员账户csrf:

在这里插入图片描述

2.8.2 代码分析

漏洞代码位于YXJCMS\application\admin\view\manager\add.html:

在提交的表单中没有任何验证方式,服务端也没有对表单的重复性进行验证,如使用csrf token等,这就导致了攻击者可以构造同样请求的表单,再利用管理员的身份验证凭证造成CSRF攻击,伪造管理员做出请求。

2.9 命令注入

2.9.1 漏洞复现

命令注入漏洞位于管理员后台的“服务器状态”功能处,管理员可以通过输入目标ip地址,然后服务端会对该ip地址进行ping操作,并返回结果。而攻击者可以通过闭合前面的命令语句并利用管道符任意执行命令,造成命令注入。

如使用payload:127.0.0.1\|ls /

可以看到成功执行了ls /语句列出来根目录下的文件。

2.9.2 代码分析

漏洞代码位于YXJCMS\application\admin\controller\Ping.php处:

代码对于传入的$target参数没有进行任何过滤和限制,直接拼接到了ping命令语句的后面,这样就导致攻击者可以利用Linux下的管道符执行自己的恶意命令,从而造成命令注入漏洞。

2.10 反序列化

2.10.1 漏洞复现

对于反序列化漏洞,由于不好结合到实际功能中,因此在管理员后台单独实现该漏洞的页面,如下:

在这里插入图片描述

构造的反序列化payload如下:

1
O:29:"app\admin\controller\start_gg":1:{s:4:"mod1";O:25:"app\admin\controller\Call":1:{s:4:"mod1";O:26:"app\admin\controller\funct":1:{s:4:"mod1";O:25:"app\admin\controller\func":1:{s:4:"mod1";O:28:"app\admin\controller\string1":1:{s:4:"str1";O:28:"app\admin\controller\GetFlag":1:{s:3:"cmd";s:10:"phpinfo();";}}}}}}

提交上述payload后可以看到成功执行了phpinfo():

2.10.2 代码分析

该反序列漏洞的源码如下:

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
<?php

namespace app\admin\controller;
use think\Controller;

class Serialize extends Controller
{
public function index()
{
if (request()->isPost()){
$payload = $this->request->post('payload');
}
unserialize($payload);
$code = file_get_contents(__FILE__);
$this->assign('code', $code);
return $this->fetch();
}
}
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke() //把对象当函数调用
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public $cmd;
public function get_flag()
{
eval($this->cmd);
}
}
?>

攻击的方式是想办法调用GetFlag类的里的get_flag()方法中的eval函数,在string1类我们可以看到,只要把$str1实例化为GetFlag类的对象,然后调用想办法调用__toString()方法即可,那就找有没有地方把对象当作字符串了。往上看,func类的__invoke()方法中有用.来进行字符串拼接的代码,那么只要把$mod1实例化为string类的对象,然后再调用该__invoke()方法即可,那就找有没有地方把对象当作函数来调用了。发现在funct类的__call()中有$s1();可以利用,只需要把$mod1实例化为func类的对象,然后再调用该__call()方法,那就找哪里调用了未声明的函数。再Call类中的test1()方法调用了不存在的test2()方法,所以只需要把$mod1实例化为funct类的对象,然后再调用该test1()方法。看到在start_gg类中的__destruct()方法中正好调用了test1()方法,那么只要$mod1实例化为Call类的对象即可。想要调用start_gg类中的__destruct()方法,只有实例化一个它的对象即可,这个对象在销毁时会自动调用__destruct()函数。

根据上述思路,写出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
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
<?php
namespace app\admin\controller;
use think\Controller;

class start_gg
{
public $mod1;
public function __construct()
{
$this->mod1 = new Call();
}
}
class Call
{
public $mod1;
public function __construct()
{
$this->mod1 = new funct();
}
}
class funct
{
public $mod1;
public function __construct()
{
$this->mod1 = new func();
}
}
class func
{
public $mod1;
public function __construct()
{
$this->mod1 = new string1();
}
}
class string1
{
public $str1;
public function __construct()
{
$this->str1 = new GetFlag();
}
}
class GetFlag
{
public $cmd;
public function __construct()
{
$this->cmd = 'phpinfo();';
}
}

$a = new start_gg();
echo serialize($a);
?>

运行上述exp.php即可得到payload。

3 漏洞修复

3.1 文件包含漏洞

对于文件包含漏洞的修复方式,一种是避免不必要的文件包含或者是直接将要包含的文件名写死在代码中。另一种就是采取白名单的方式,将需要包含的文件名放在一个白名单中,当进行文件包含前,先判断要包含的文件名是否出现在白名单内,若未出现在白名单中,则不允许包含。

这样采取白名单限制的方式,可以说是一种比较安全的修复方式了,具体修复代码如下:

3.2 SSRF

对于SSRF的修复主要是对传入的$url参数进行一定的限制和过滤,过滤主要在下面三部分:

  • 限制只允许使用http和https两种协议。

  • 不允许url的host为内网ip地址。

  • 传入的$url必须符合正常的url格式,

因此我们编写check_inner_ip函数对$url进行验证:

3.3 XXE

在不影响使用的情况下,应该考虑在PHP的配置中禁止外部实体的引用,以防止由于实体的引入而导致的文件任意读取、拒绝服务攻击等后果。具体的,可以修改如下的配置:libxml_disable_entity_loader(true);

这样即可以防止攻击者通过XXE漏洞读取的内部文件。

3.4 弱口令

对于弱口令的修复与防御主要在于加强用户或管理员的安全意识,让他们对设置的密码进行隐藏,并且经常修改密码。

而从系统方面也可以在注册的时候,就限制密码的长度或者必须包含字符等要求,但是过于复杂的密码也不便于用户的记忆,因此应该权衡好安全性与用户体验直接的平衡关系。

3.5 存储型XSS

对于XSS的防御比较直接,只需要对相应的字符进行转移即可,在这里我们可以使用PHP中自带的htmlspecialchars()函数对传入的title进行处理,转移其中的特殊字符:

3.6 文件上传漏洞

由于本Web应用系统本身已经对文件上传进行了一定的限制,问题主要出在管理员有权限将php文件加到运行上传文件列表中。但实际上,我们要做的是即使是管理员将php设置为运行上传,系统依旧不接受php文件。

修复代码如下,在其他过滤条件的基础上,对后缀为php的文件进行了限制,并返回“禁止上传非法文件”:

3.7 SQL注入

对于SQL注入最直接的修复方式,就是禁止一切的sql语句直接拼接,而是使用ThinkPHP为我们提供好的操作数据库的类,这样避免了对SQL语句的直接操作,即方便了使用,也增加了安全性与可移植性。

除此之外,在对于id参数进行查询的时候,因为id为数字,所以我们可以通过id/d的方式,将接收的参数转换为整型进行操作,这样可以进一步限制攻击,增加安全性:

3.8 CSRF

对于CSRF的修复主要在于我们需要对每次传递到后端的表单进行验证,判断其不是攻击者构造或重放的请求。通常的修复方式是在前端表单每次提交的时候为其加上一个随机的token参数,然后服务端在解析收到的表单前,对这个token进行验证,验证通过才可以进行下一步操作,否则则判断这个表单请求是不可信的。

前端修复代码如下,加上了随机参数__token__:

后端还需要对该token进行验证:

如果收到的__token__与生成的不一致,则返回token验证失败,表单不被接受。

3.9 命令注入

原来的命令执行前未对传入的参数进行任何的过滤,而我们的修复代码可以使用PHP中自带的两个过滤函数:

  • escapeshellarg():把字符串转码为可以在 shell 命令里使用的参数。

  • escapeshellcmd():对字符串中可能会欺骗 shell
    命令执行任意命令的字符进行转义。

通过这两个函数结合使用,攻击者无法利用拼接的方式加入自己的恶意命令,从而对命令执行漏洞进行了修复:

3.10 反序列化

对于反序列化漏洞来说,没有特别针对的防御与修复方式,唯一安全的架构模式是不接受来自不受信源的序列化对象,或使用只允许原始数据类型的序列化,即尽量不要直接反序列化用户传入的序列化串。其次是尽量不要使用eval、system等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
<?php

namespace app\admin\controller;
use think\Controller;

class Serialize extends Controller
{
public function index()
{
if (request()->isPost()) {
$payload = $this->request->post('payload');
}
unserialize($payload);
$code = file_get_contents(__FILE__);
$this->assign('code', $code);
return $this->fetch();
}
}

class start_gg
{
public $mod1;
public $mod2;

public function __destruct()
{
$this->mod1->test1();
}
}

class Call
{
public $mod1;
public $mod2;

public function test1()
{
$this->mod1->test2();
}
}

class funct
{
public $mod1;
public $mod2;

public function __call($test2, $arr)
{
$s1 = $this->mod1;
$s1();
}
}

class func
{
public $mod1;
public $mod2;

public function __invoke()
{
$this->mod2 = "字符串拼接" . $this->mod1;
}
}

class string1
{
public $str1;
public $str2;

public function __toString()
{
$this->str1->get_flag();
return "1";
}
}

class GetFlag
{
public $cmd;

public function get_flag()
{
$blacklist = ['phpinfo', 'dl', 'exec', 'system', 'passthru', 'popen', 'pclose, proc_open', 'proc_nice', 'leak',
'proc_terminate', 'proc_get_status', 'proc_close', 'apache_child_terminate', 'escapeshellcmd', 'shell_exec',
'crack_check', 'crack_closedict', 'crack_getlastmessage', 'crack_opendict', 'psockopen', 'symlink',
'ini_restore', 'posix_getpwuid', 'pfsockopen', 'file_get_contents', 'file_put_contents', 'readfile'];
if (!in_array($this->cmd, $blacklist)) {
eval($this->cmd);
}
}
}
?>

使用黑名单过滤的方式,对传入的$cmd进行过滤,若只有其不在黑名单的敏感函数操作中时才能执行eval函数,这样就避免了攻击者利用反序列化进行命令执行、文件读取等危险操作,一定程度上防御了反序列化漏洞。

4 docker部署

4.1 安装docker

在ubuntu中可以使用apt-get的方式进行安装,具体安装命令如下:

1
2
3
4
5
6
7
8
9
10
# step 1: 安装必要的一些系统工具
sudo apt-get update
sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-common
# step 2: 安装GPG证书
curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
# Step 3: 写入软件源信息
sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"
# Step 4: 更新并安装 Docker-CE
sudo apt-get -y update
sudo apt-get -y install docker-ce

4.2 安装docker-compose

由于本次部署不止一个服务,所以为了避免多次编写dockerfile和多次启动,选择使用docker-compose进行部署,安装过程如下:

1
2
3
4
# 安装docker-compose
sudo curl -L https://github.com/docker/compose/releases/download/1.25.5/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
# 赋予执行权限
sudo chmod +x /usr/local/bin/docker-compose

4.3 部署启动

在部署前我们需要写相应的脚本,包括:

  • PHP相关配置(php目录下): Dockerfile、php.ini、php-fpm.conf

  • Nignx相关配置(nginx目录下):conf.d目录、nginx.conf

  • Mysql相关配置(mysql目录下):init.sql、my.cnf

  • 系统源代码位于src目录下

  • 服务部署:docker-compose.yml

具体的脚本内容,可以查看相关文件。

部署的时候,在docker-compose.yml同目录下执行sudo docker-compose
up命令一键启动服务,第一次部署时由于需要下载相关的镜像所以速度比较缓慢,如下:

等镜像都按照完成后,以后再次使用sudo docker-compose
up命令启动服务时就会非常的快捷方便: