这几年,开发人员的安全意识与安全开发水平都有了很大程度的提高。像文件上传、SQL注入漏洞,在网络安全比较受重视的企业,差不多都已经杜绝了,即便有出现,也是凤毛菱角了。目前,部分企业网络安全目前面临的另一大挑战就是越权漏洞,特别是大型系统,功能模块接口特别多,如果未对权限进行全局校验,则难免会有所遗漏,最终出现越权漏洞。
一、越权漏洞定义及分类
越权,从字面意思不难理解,主要有以下几种可能:
1、未授权访问:本来没有账号(即没有某个功能权限),但是通过越权操作,获取了某个功能权限;
2、水平越权:本来有个账号(即只能操作自己的数据,比如增删改查),但是通过越权操作,能操作其他同等权限账号的数据。
3、垂直越权:本来有个账号只有小权限,但是通过越权操作,获取了大权限。
二、测试方法
2.1、未授权访问
最简单直接的测试方法是:不登录用户账号,直接访问要测试的功能模块(正常情况是需要登录才能访问),如果能正常访问,则说明存在漏洞。
修改返回包的测试方法:日常测试时,有些开发人员在前端校验返回包中某个参数的值,在返回包中有类似status=false的值,可将改为status=true(也有碰到过code=-1改为code=0或者1的情况);遇到过最奇葩的一次是:不登录账号直接访问那个系统的后台首页index.html时,返回包主体中通过js脚本,通过script和location跳转到登录页面login.html,通过burpsuite拦截响应表,再将那段用于页面跳转的js脚本删除,就能进入后台。(虽然这种修改返回包的方法最后也没有登录账号,但是进入后台后,能访问一些内部公共的敏感功能模块和信息)。
部分系统,用户信息并不是从sessionid中获取登录账号;而是在cookie中添加一个类似userid的键值对,再从中识别用户。从而可以尝试通过cookie伪造的方法进行测试。
2.2、水平越权
水平越权常见于业务系统中,对用户信息或者订单信息进行增删改查操作时,由于用户编号或者订单编号有规律可循(有序递增,订单编号常发现以日期开头后面再接几位有序增长的数字,类似20200520xxxx1,20200520xxxx2),测试人员通过burpsuite的 intruder对目标参数进行遍历测试即可
2.3、垂直越权
日常安全测试时,大多发现是未授权访问和水平越权。只碰到过一次将cookie中的isadmin=0改为isadmin=1,获取到管理员权限。苦于黔驴技穷,有幸某天灵光一现,想到测试任意文件上传和下载漏洞时,都能借助../和./进行绕过,垂直越权时,何不也尝试借助../和./进行绕过呢,最终果然发现新大陆。最后经过反复测试发现以下几种垂直越权测试方法。
http://www.xxxx.com/admin//admin.do
http://www.xxxx.com/admin/admin.do%23
http://www.xxxx.com/js/../admin/admin.do
http://www.xxxx.com/admin/./admin.do
三、修复方案
建议做一个过滤器,对权限进行全局校验(每次调用某个接口时,可先对权限进行校验)。大体流程是:第一步清洗URL地址,并提取Api接口名称;第二步从session中提取当前登录用户的userid;第三步提取当前用户的角色id;第四步判断当前用户对应的角色是否有权限访问当前Api接口(检查垂直越权);最后判断当前登录用户是否对目标对象有操作权限(检查水平越权)。
首先是获取URL地址中的Api接口名称,这里对URL地址进行了一次URL解码,还是存在绕过的风险,因为当用户%2523时,就可能绕过。
public static String GetApiName(String Url) throws Exception {
String DecodeUrl = URLDecoder.decode(Url,"UTF-8");URL url = new URL(DecodeUrl);String ApiUrl = url.getPath(); if(ApiUrl !=null){
String[] ApiPath = ApiUrl.split("/");String ApiName = ApiPath[ApiPath.length-1]; return ApiName;}
return null;}
获取userid时,建议从session中获取,而不是在cookie中再新建一个userid字段,用于标识用户身份。
HttpSession session = ServletActionContext.getRequest().getSession();String userId = session.getAttribute("userId");*/
从session中提取userid后,就要查询与之对应的角色id
//UserId为用户id,RoleId为角色ID,通过UserId获取roleidpublic static int GetRoleId(int UserId) {
Connection conn = Connect();PreparedStatement st = null;ResultSet rs = null; int RoleId = 0; try {
// 查询接口访问的角色id,roleid为权限表里的角色ID字段,apiname为权限表里的API接口名称IDString sql = "select roleid from usertable where userid = ?";st = conn.prepareStatement(sql);// 这里使用PreparedStatementst.setInt(1, UserId);rs = st.executeQuery(); if(rs.next()){
RoleId = rs.getInt("roleid"); return RoleId;}
} catch (Exception e) {
e.printStackTrace(); throw new RuntimeException("查询失败!");}
return RoleId;}
校验垂直越权时,判断当前用户是否对指定的接口有访问权限,U_RoleId为用户名对应的角色id,A_RoleId为Api接口对应的角色id,Api_Name为用户尝试访问的API接口名称(这里在系统架构评审,安全设计阶段,就要检查数据库的权限表设置时,Api接口是否有指定对应的角色id)
public static boolean CheckUpPrivilege(int UserId, String Api_Name) {
Connection conn = Connect();PreparedStatement st = null;ResultSet rs = null; int U_RoleId = GetRoleId(UserId); int A_RoleId = 0; try {
String sql = "select roleid from user_role where apiname = ?";st = conn.prepareStatement(sql);// 这里使用PreparedStatementst.setString(1, Api_Name);// 执行sql命令roleid和Role_Id,判断用户是否有权限访问对应的接口地址rs = st.executeQuery(); if(rs.next()) {
A_RoleId = rs.getInt("roleid");// 通过比较,当用户角色id大于等于接口指定的角色id是,可以访问,部分特定接口只有指定的角色才能访问,可直接限定if (U_RoleId >= A_RoleId) {
return true;} else {
return false;}
}
} catch (Exception e) {
e.printStackTrace(); throw new RuntimeException("查询失败!");}
return false;}
最后再判断一下是否是水平越权,S_UserId为当前登录用户的userid,P_UserId为目标对象对应的userid,比如对订单信息进行操作时,可以先通过订单号提取与之对应的userid,再进行判断(当然,订单表,在系统架构评审,安全设计阶段,就要检查订单号是否有指定对应的用户id)。
public static boolean CheckLevelPrivilege(int S_UserId, int P_UserId) {
if(S_UserId == P_UserId){
return true;}
else{
return false;}
}
四、最后
类似订单号这种参数,生成时要无规律可循,可以通过hash算法进行加密;或者请求的数据包额外附带一个参数,比如token,从而防止重放和遍历订单号这类攻击(篡改订单号)。
*本文作者:freebuf01,转载请注明来自FreeBuf.COM