由于微信公众号推送机制改变了,快来星标不再迷路,谢谢大家!
第一章 前言
软件安全开发生命周期(SDL,Security Development Lifecycle)是一个旨在帮助开发人员构建更安全的软件的软件开发过程,其核心理念就是将安全考虑集成在软件开发的每一个阶段:需求分析、设计、编码、测试和维护,以减少漏洞并提高系统安全性,本文将详细叙述安全编码的原则,同时也为需求分析、设计提供参照。
第二章 安全编码原则
在需求分析、进行系统架构设计时,应当全面考虑用户、APP/浏览器、后台服务以及各层通讯之间可能存在的安全问题,开展安全防护设计。
2.1 上线前通用安全开发要求
1. 使用经测试和可信的平台/框架代码开发应用程序,并保持对所依赖的框架更新,以避免出现已知漏洞。
2. log4j等第三方组件应当升级至最高版本,防止攻击者利用第三方组件漏洞进入内网。
3. 应用程序就避免于页面(HTML、JavaScript)中包含技术性注释语句、功能说明、个人信息或解释等。
4. 代码上线前,应删除代码中相应的测试内容,包括但不限于:测试页面、测试代码等。
5. 应用系统部署后,应删除默认部署页面,禁止留存SVN/Git相关文件、备份文件等。
6. 如果应用软件部署在客户端,例如移动APP,应使用混淆、签名、加固等措施防止逆向获取源代码。
7. 禁止使用phpMyAdmin、Struts 1/2、Fastjson组件
8. 禁止德鲁伊、Actuator、nacos、dubbo、minio、kibana、zabbix、spark、hadoop、ssh、ftp、telnet等特殊组件对外网暴露。
9. 禁止使用应用组件缺省账号和密码,管理员账号需满足大于8位且有大小写字母、数字、特殊字符;密码不能与用户名相同;避免密码中包含公司特征密码等。
10. 禁止swagger等接口文档对外开放.
2.2 输入验证
1. 必须在可信赖的环境(例如后台服务)完成数据校验。若在前端进行数据校验存在被绕过篡改的风险。
2. 判断输入是否符合预期的数据类型、长度、数据范围等。
3. 应当采用白名单形式进行输入校验,防止出现url跳转、ssrf等漏洞。规范/不规范代码示例参见3.21。
4. 对于SQL注入、XSS、命令注入、表达式注入等常见的危险字符 <>'"%|;&/ 应结合业务场景进行过滤。规范/不规范代码示例参见3.1、3.16、3.17、3.20。
5. 相关输入验证和过滤处理应争取从框架层面进行全局或者统一的处理,避免前后处理不一致而导致的绕过。
6. 下游服务在使用内部服务时,应该考虑到即使是内部服务传递的数据也有可能夹带恶意参数。例如:传入的参数被用来执行系统命令、传入的参数被用来下载文件等,应进行风险评估后,根据场景对入参实施校验。
2.3 身份验证与会话管理
1. 避免在URL中传递会话标识。
2. 控制用户登录鉴权的Cookie 应当设置HttpOnly属性以防止被XSS漏洞/JavaScript操纵泄漏。
3. 实现全站HTTPS后,Cookie应当设置secure属性。使得浏览器仅在安全加密连接时才能传送使用该cookie。
4. 应设置会话令牌有效期,建议公网系统会话有效时间不超过30分钟.规范/不规范代码示例参见3.15。
5. 用户注销时应当立即清理当前用户会话。
6. 在用户执行关键或者不可逆的操作,如交易时、修改密码之前,应再次验证用户身份,以减少不安全会话带来的损失,防范跨站请求伪造攻击。
7. 禁止userid、roleid、usertype等关键身份鉴别参数被外部控制,建议从会话中获取这些参数。规范/不规范代码示例参见3.2。
2.4注册/登录/忘记密码
2.4.1 自行实现注册/登录/忘记密码功能安全要点
1. 管理平台禁止自行实现注册功能。
2. 2B业务系统注册(包括试用)需配有审核流程。
3. 必须实现注册功能的场景,其注册接口应使用短信或邮箱验证码等进行身份认证。
4. 注册时需限制用户名合法字符和长度;密码需禁止使用弱口令;密码长度应大于8位且有大小写字母、数字、特殊字符;密码不能与用户名相同;避免密码中包含公司特征密码等。
5. 登录失败时不应返回详细提示,需返回用户名或密码错误,防止猜解用户名。
6. 登录验证码每校验过一次应当立即失效防止验证码重用。
7. 后台管理页面需记录成功登录用户名和IP、时间。
8. 当尝试登录IP 不在历史常登录IP地理位置时,应进行多因素二次验证用户身份,防止用户因密码泄漏被窃取账户。
9. 密码输入界面不应以明文形式显示密码。
10. 应校验手机号/邮箱和验证码的关联性,确保验证码只能被一个手机号/邮箱使用。
2.4.2 敏感/核心业务必须采用登录态隔离
1. 具有传播效应的写操作或者拉取核心数据的读操作功能的敏感业务需采用登录态隔离方案,通过公司统一登录平台下发业务私有的凭证,作为用户登录态的凭证,不再让单一的凭证成为多个业务的通行证。
2.5 访问控制
1. 游客身份浏览页面,不应使用预埋的账号进行处理(如专门开通一个账号给游客认证使用)。
2. 接口返回的数据需遵循最小化原则,仅返回下游接口需要的参数。
3. 服务端应对验证登录尝试的频率进行限制,连续多次登录失败的,应当对其进行限制:
(1)限制IP地址能够进行验证尝试的频率和次数。5秒内在超过5次后,封禁IP地址半小时。
(2)限制账号能够进行验证尝试的频率和次数。在超过5次后,锁定账户半小时或单个用户口令失败3次后弹出验证码。
4. 新注册用户需要遵循权限最小化原则进行权限分配。
5. 短信验证码应采用防爆破机制,例如一分钟之内只能发送一次,另外需通过加锁的方式防止条件竞争,谨防短信轰炸。规范/不规范代码示例参见3.9。
6. 需防止将参数置空后进行全量数据查询。例如,使用userid查询订单,将userid置空后,可以查到平台所有用户的订单。规范/不规范代码示例参见3.3。
7. 隶属于用户个人的页面或者功能需进行严格的权限控制校验,防止越权漏洞(同级、跨级用户之间的越权),避免任意使用、访问、修改、删除他人的数据,比如根据2.7章节第三条中的id参数查看他人的私信内容、修改他人的订单等。
8. 确保只有授权的用户才能访问秘密数据或敏感数据,这类数据包括:与用户信息或应用软件自身安全密切相关的状态信息、文件(例如heapdump)或其他资源、受保护的URL、受保护的功能、应用程序数据以及与安全相关的配置信息等。应避免未授权访问。
2.6 传输安全与加密
1. 增、删、改操作需使用POST方法进行提交。
2. 无特殊的理由,所有的Web页面和HTTP API接口必须通过HTTPS进行,且版本在TLS 1.2 及以上。
3. 算法选择:对称加密推荐使用AES-128及其以上算法,RC系列算法推荐使用RC5、RC6;非对称加密推荐使用RSA-2048及以上算法、DSA-2048及以上算法、ECC-256及以上算法;哈希算法推荐使用SHA-2(SHA-256)或SHA-3。不推荐使用MD5和SHA-1。
2.7 数据保护
1. 不要在客户端js等、LocalStorage上明文保存账号、密码、密钥、内网ip地址或其他敏感信息。
2. 涉及个人隐私的敏感信息需加密存储并且脱敏后显示给用户。
3. 用于标记资源的ID参数(包括userid、roleid、orderid等id参数)不能是数序数字以防止被遍历,要保证id参数是充分随机的编码,例如,用户编码使用16位随机编码,商品订单编码可以考虑32位随机编码。规范/不规范代码示例参见3.7。
4. 涉及用户隐私、高价值信息的接口需要对用户UID、IP的来源做频率控制, 防止核心数据被黑产、竞争对手爬取。
2.8 文件上传、下载
1. 建议搭建本地文件服务器,以满足上传下载的需求,对于上传下载性能要求比较高的业务系统,可适当考虑使用云上存储。
2. 文件上传前需验证用户身份。
3. 根据业务场景需要,必须以白名单形式校验限制上传的文件类型。规范/不规范代码示例参见3.19。
4. 文件上传校验需验证文件包头(content-type)信息是否匹配,需限制合适的文件大小。规范/不规范代码示例参见3.19。
5. 限制解压后的文件数量和总大小。在解压文件之前,检查解压后的文件数量和总大小是否超过预设的阈值。如果超过阈值,则拒绝解压并给出警告,以防止恶意用户上传压缩炸弹。规范/不规范代码示例参见3.13。
6. 文件禁止保存在Web容器内。
7. 使用第三方存储服务进行文件存储需要注意权限配置检查,避免由于使用默认配置而导致的文件可直接遍历泄露等问题;如腾讯云cos服务过去默认新建bucket是public list权限,业务直接使用默认配置会导致存储的文件可被遍历并任意下载。
8. 需要上传至OSS存储的业务系统,禁止直传的方式上传oss,禁止将密钥写入客户端文件中。
9. 为了防止文件名中有路径符号导致的文件穿越,需采用安全的随机算法对上传文件进行重命名,长度建议为16位以上。规范/不规范代码示例参见3.7。
10. 如果不能进行文件重命名的场景,需验证下载文件名是否含有../和.. ,防止路径遍历导致的任意文件下载。规范/不规范代码示例参见3.18。
11. 文件上传后不应向用户返回文件保存的绝对路径,避免泄露服务器信息。
12. 验证文件的真实路径是否在允许下载范围内。
13. 字节/字符流及时关闭,例如FileInputStream(字节流)、FileOutputStream(字节流)、FileReader(字符流)、FileWriter(字符流)、Socket(字节流)、ServerSocket(字节流)。规范/不规范代码示例参见3.10。
2.9 支付逻辑
1. 支付相关接口需通过后端对入参进行严格的校验,应考虑到如下场景:
(1) 禁止用户修改订单的金额
(2) 禁止用户修改支付状态(应保证支付status,不可控)
(3) 禁止用户对优惠券并发领取,应指定每人只能领取指定数量的优惠券
(4) 禁止用户对优惠券重用,对使用过的优惠券进行判断,防止恶意用户使用失效的券id进行再次购买
(5) 优惠券id需采用安全的随机算法生成,长度建议为16位以上。以防止攻击者对券id进行遍历。规范/不规范代码示例参见3.7。
(6) 使用优惠券时,需判断优惠券归属,防止恶意用户越权使用他人优惠券。
(7) 服务端需对数量和金额进行合理的上限设定,以防止整数溢出,边界值为1073741824和2147483647。规范/不规范代码示例参见3.4。
(8) 防止金额的四舍五入,例如1.4元,四舍五入后变成1元
(9) 禁止用户修改商品的数量为非整数(禁止传入小数值,因为将可能导致四舍五入)
(10) 禁止通过频繁的注销--注册等方式,获取体验日期的重置
(11) 禁止进行并发提现
2.10 异常处理与日志审计
1. 精确捕获异常,并对捕获的异常进行恰当的处理,避免在捕获异常后不做任何处理
2. 当系统报错时,不要将系统产生的异常信息直接反馈给用户,包括但不限于:系统的详细信息、会话标识、账号信息、调试或堆栈跟踪信息、源代码片段等。规范/不规范代码示例参见3.8。
3. 应创建默认的错误页面,使用通用的错误消息提示,防止攻击者从出错页面中得到一些敏感的系统信息。
4. 不要在日志中保存敏感信息。对日志记录的内容进行审核,防止将关键级敏感信息写入日志,日志记录中不得以任何形式保存密码、密钥等高敏感性数据,其他敏感信息仅应在脱敏或加密的前提下保存。
5. 对日志中的特殊元素进行过滤和验证。确保日志记录中的不可信数据不会在查看界面或者运行软件时以代码的形式被执行,例如,r n等换行符的录入,可能会造成日志内容换行,从而篡改日志内容。如果日志中存在恶意代码,在页面读取展示时,可能会造成恶意代码执行,如XSS攻击、一句话木马等。
6. 根据《网络安全法》第二十一条规定,采取监测、记录网络运行状态、网络安全事件的技术措施,并按照规定留存相关网络日志不少于六个月。
2.11编码相关
1. 废弃的冗余代码应该及时删除。
2. 代码中不应存在永真和永假代码。
3. 禁止将用户名密码、密钥、secret等关键信息写死在代码中,尽量使用环境变量或服务器配置文件存储敏感信息。
4. 禁止使用默认或容易被猜解的密钥,例如shiro默认密钥(kPH+bIxk5D2deZiIxcaaaA==),nacos的jwt默认密钥(SecretKey012345678901234567890123456789012345678901234567890123456789),apisix默认密钥(edd1c9f034335f136f87ad84b625c8f1)。应当使用安全的随机算法进行生成。密钥长度建议32位以上。规范/不规范代码示例参见3.7。
5. 禁止危险的方法或函数public。规范/不规范代码示例参见3.11。
6. 需遵守正确的行为次序,避免早期放大攻击数据。规范/不规范代码示例参见3.12。
7. 防止边界操作发生越界。规范/不规范代码示例参见3.14。
8. 防止数值范围比较时遗漏边界值检查导致的逻辑漏洞。规范/不规范代码示例参见3.6。
9. 防止发生除零错误导致的程序奔溃。规范/不规范代码示例参见3.5。
第三章 安全编码
本章节给出了安全编码中常见问题的代码示范,均以java为例。
3.1命令注入
3.1.1错误代码示范
避免关键状态数据被外部控制,以下给出了不规范用法
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandInjectionExample {
public static void main(String[] args) {
String command = "ping " + args[0]; // 将用户输入的参数拼接到命令中
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
上述直接将用户的输入作为exec执行的参数,将导致命令注入
3.1.2修复
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandInjectionExample {
public static void main(String[] args) {
String command = "ping " + sanitizeInput(args[0]); // 使用sanitizeInput方法过滤用户输入
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String sanitizeInput(String input) {
// 过滤掉可能导致命令注入的字符,例如分号、管道符等
return input.replaceAll("[;|&<>{}]", "");
}
}
使用sanitizeInput方法将用户输入的特殊字符进行过滤,防止命令注入的发生
3.2验证未经校验和完整性检查的Cookie
3.2.1错误代码示范
Cookie[] cookies = request.getCookies();
String userRole = null;
for (int i = 0; i < cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("role")) {
userRole = c.getValue();
}
}
上述代码示例从浏览器cookie读取一个值确定用户的角色,攻击者容易修改本地存储cookie中的“role”值,可能造成特权升级。
3.2.2修复
使用HttpSession对象来存储用户ID和角色信息
@WebServlet("/StoreUserInfo")
public class StoreUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 设置用户ID和角色信息
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
// 输出提示信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID和角色信息已存储到会话中");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
从会话中获取用户的ID或角色信息:
@WebServlet("/GetUserInfo")
public class GetUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 从会话中获取用户ID和角色信息
String userId = (String) session.getAttribute("userId");
String role = (String) session.getAttribute("role");
// 输出用户ID和角色信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID: " + userId);
response.getWriter().println("角色:" + role);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
3.3内存溢出
3.3.1错误代码示范
@Override
public List<ThirdPaySlave> selectByPayNoThird(String payNoThird) {
List<ThirdPaySlave> result = null;
try {
result = thirdPaySlaveMapper.selectByPayNoThird(payNoThird);
} catch (Exception e) {
logger.error("Error occurred while selecting ThirdPaySlave by payNoThird: " + payNoThird, e);
throw new RuntimeException("Failed to select ThirdPaySlave by payNoThird", e);
}
return result;
}
当数据量比较大,且payNoThird为空的时候,selectByPayNoThirdsql语句可能会执行的非常慢,多次调用后,可能会导致内存撑爆,系统奔溃。
3.3.2修复
@Override
public List<ThirdPaySlave> selectByPayNoThird(String payNoThird) {
if (StringUtils.isBlank(payNoThird)) {
logger.warn("payNoThird is blank");
return new ArrayList<>();
}
List<ThirdPaySlave> result = null;
try {
result = thirdPaySlaveMapper.selectByPayNoThird(payNoThird);
} catch (Exception e) {
logger.error("Error occurred while selecting ThirdPaySlave by payNoThird: " + payNoThird, e);
throw new RuntimeException("Failed to select ThirdPaySlave by payNoThird", e);
}
return result;
}
对传入的 pay_no_third 参数进行判空,如果为空,就返回空 List 集合。这样就避免了内存溢出的问题。
3.4整数溢出
3.4.1错误代码示范
public class IntegerOverflowExample {
public static void main(String[] args) {
int a = 2147483647; // int类型的最大值
int b = 1;
int sum = a + b; // 这里会发生整数溢出
System.out.println("Sum: " + sum);
}
}
当i的值为-2147483648时,再减1会导致整数溢出,因为int类型的取值范围是-2147483648到2147483647。
3.4.2修复
public class IntegerOverflowExample {
public static void main(String[] args) {
long a = 2147483647L; // long类型的最大值
long b = 1;
long sum = a + b; // 使用long类型来存储结果,避免整数溢出
System.out.println("Sum: " + sum);
}
}
我们将变量a和b的类型都改为long类型,并将它们相加得到sum。由于long类型的取值范围比int类型更大,所以可以避免整数溢出的问题。最后,我们打印出sum的值,结果为2147483648,这是正确的结果。
3.5除零错误
3.5.1错误代码示范
public class Main {
public static void main(String[] args) {
int a = 10;
int b = 0;
int result;
result = a / b; // 这里会引发运行时异常ArithmeticException
System.out.println("Result: " + result);
}
}
将一个整数除以零。由于除数为零,会引发运行时异常ArithmeticException。但是,这个示例没有使用try-catch语句来捕获这个异常,因此程序会崩溃并抛出异常信息
3.5.2修复
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandInjectionExample {
public static void main(String[] args) {
String command = "ping " + sanitizeInput(args[0]); // 使用sanitizeInput方法过滤用户输入
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String sanitizeInput(String input) {
// 过滤掉可能导致命令注入的字符,例如分号、管道符等
return input.replaceAll("[;|&<>{}]", "");
}
}
0
尝试将一个整数除以零。由于除数为零,会引发运行时异常ArithmeticException。为了避免程序崩溃,我们可以使用try-catch语句来捕获这个异常,并采取相应的措施。
3.6数值范围比较时遗漏边界值检查
3.6.1错误代码示范
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandInjectionExample {
public static void main(String[] args) {
String command = "ping " + sanitizeInput(args[0]); // 使用sanitizeInput方法过滤用户输入
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String sanitizeInput(String input) {
// 过滤掉可能导致命令注入的字符,例如分号、管道符等
return input.replaceAll("[;|&<>{}]", "");
}
}
1
我们比较了两个整数a和b的大小。但是,我们没有检查a和b是否相等。
3.6.2修复
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandInjectionExample {
public static void main(String[] args) {
String command = "ping " + sanitizeInput(args[0]); // 使用sanitizeInput方法过滤用户输入
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String sanitizeInput(String input) {
// 过滤掉可能导致命令注入的字符,例如分号、管道符等
return input.replaceAll("[;|&<>{}]", "");
}
}
2
在这个修复后的示例中,我们添加了一个额外的条件来检查a和b是否相等。
3.7采用能产生充分信息熵的算法或方案
3.7.1错误代码示范
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandInjectionExample {
public static void main(String[] args) {
String command = "ping " + sanitizeInput(args[0]); // 使用sanitizeInput方法过滤用户输入
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String sanitizeInput(String input) {
// 过滤掉可能导致命令注入的字符,例如分号、管道符等
return input.replaceAll("[;|&<>{}]", "");
}
}
3
使用Java的Random类来生成随机数,并将它们拼接成一个字符串作为熵。但是,这个示例存在一个问题:它只生成了0和1两个数字,而没有覆盖所有可能的值。
3.7.2修复
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandInjectionExample {
public static void main(String[] args) {
String command = "ping " + sanitizeInput(args[0]); // 使用sanitizeInput方法过滤用户输入
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String sanitizeInput(String input) {
// 过滤掉可能导致命令注入的字符,例如分号、管道符等
return input.replaceAll("[;|&<>{}]", "");
}
}
4
使用Java的SecureRandom类来生成随机数,并将它们转换为十六进制字符串作为熵。SecureRandom类是Java标准库中的一个安全随机数生成器,它提供了比Random类更高的安全性和随机性。在生成熵时,我们首先创建一个长度为ENTROPY_LENGTH的字节数组,然后使用SecureRandom对象填充该数组。最后,我们将每个字节转换为两个十六进制字符,并将它们拼接成一个字符串作为最终的熵。这个修复后的示例可以生成一个包含16个随机十六进制字符的熵字符串,具有足够的随机性和安全性,可以用于加密、安全认证等场景。
3.8 信息泄露
3.8.1错误代码示范
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandInjectionExample {
public static void main(String[] args) {
String command = "ping " + sanitizeInput(args[0]); // 使用sanitizeInput方法过滤用户输入
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String sanitizeInput(String input) {
// 过滤掉可能导致命令注入的字符,例如分号、管道符等
return input.replaceAll("[;|&<>{}]", "");
}
}
5
我们从config.properties文件中读取数据库的用户名和密码,逻辑处理过程中如果发生错误,则输出账号密码,这是一个典型的信息泄露错误,因为敏感信息(如数据库用户名和密码)被直接输出到了不安全的地方。
3.8.2修复
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandInjectionExample {
public static void main(String[] args) {
String command = "ping " + sanitizeInput(args[0]); // 使用sanitizeInput方法过滤用户输入
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String sanitizeInput(String input) {
// 过滤掉可能导致命令注入的字符,例如分号、管道符等
return input.replaceAll("[;|&<>{}]", "");
}
}
6
使用了SLF4J日志框架来记录敏感信息。我们将输出语句替换为logger.info()和logger.error()方法,并将异常对象作为第二个参数传递给logger.error()方法。这样,敏感信息就被安全地记录到了日志文件中,而不会被输出到不安全的地方。
3.9加锁检查缺失
3.9.1错误代码示范
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandInjectionExample {
public static void main(String[] args) {
String command = "ping " + sanitizeInput(args[0]); // 使用sanitizeInput方法过滤用户输入
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String sanitizeInput(String input) {
// 过滤掉可能导致命令注入的字符,例如分号、管道符等
return input.replaceAll("[;|&<>{}]", "");
}
}
7
increment()方法没有使用任何同步机制来保护对count变量的访问。这可能导致多个线程同时调用increment()方法时发生条件竞争,从而导致计数器的结果小于20000。
3.9.2修复
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandInjectionExample {
public static void main(String[] args) {
String command = "ping " + sanitizeInput(args[0]); // 使用sanitizeInput方法过滤用户输入
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String sanitizeInput(String input) {
// 过滤掉可能导致命令注入的字符,例如分号、管道符等
return input.replaceAll("[;|&<>{}]", "");
}
}
8
使用了synchronized关键字来修饰increment()方法,这样在同一时间只有一个线程可以执行该方法,从而避免了并发问题。
3.10文件流未及时关闭
3.10.1错误代码示范
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandInjectionExample {
public static void main(String[] args) {
String command = "ping " + sanitizeInput(args[0]); // 使用sanitizeInput方法过滤用户输入
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String sanitizeInput(String input) {
// 过滤掉可能导致命令注入的字符,例如分号、管道符等
return input.replaceAll("[;|&<>{}]", "");
}
}
9
打开了一个文件输入流FileInputStream,但是在finally块中忘记关闭它。这会导致资源泄漏,因为打开的文件描述符没有被释放,可能会导致系统资源耗尽。
3.10.2修复
Cookie[] cookies = request.getCookies();
String userRole = null;
for (int i = 0; i < cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("role")) {
userRole = c.getValue();
}
}
0
在finally块中添加了对文件输入流的关闭操作。首先检查fis是否为null,如果不为null,则调用fis.close()方法来关闭文件输入流。这样可以确保即使发生异常,文件输入流也会被正确关闭,避免资源泄漏。
3.11禁止暴露危险的方法或函数
3.11.1错误代码示范
Cookie[] cookies = request.getCookies();
String userRole = null;
for (int i = 0; i < cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("role")) {
userRole = c.getValue();
}
}
1
上面代码示例中,方法 removeDatabase()将删除输入参数中指定名称的数据库。示例中的方法是被声明为public,因此会被暴露给应用程序中的任何类。在应用程序内删除一个数据库被视为一个危险的操作,应限制访问危险的方法。
3.11.2修复
Cookie[] cookies = request.getCookies();
String userRole = null;
for (int i = 0; i < cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("role")) {
userRole = c.getValue();
}
}
2
该方案通过声明方法为private来完成,只将它暴露给封闭的类。
3.12遵守正确的行为次序避免早期放大攻击数据
3.12.1错误代码示范
Cookie[] cookies = request.getCookies();
String userRole = null;
for (int i = 0; i < cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("role")) {
userRole = c.getValue();
}
}
3
上述代码首先读取指定文件到内存中,然后查看用户是否有访问权限。如果用户不被允许访问该文件,则文件读取到内存中非常浪费资源,可能会造成早期放大攻击。
3.12.2修复
Cookie[] cookies = request.getCookies();
String userRole = null;
for (int i = 0; i < cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("role")) {
userRole = c.getValue();
}
}
4
先校验是否有数据权限,再去访问数据
3.13压缩炸弹防护
3.13.1错误代码示范
Cookie[] cookies = request.getCookies();
String userRole = null;
for (int i = 0; i < cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("role")) {
userRole = c.getValue();
}
}
5
程序未对压缩文件解压后的大小进行判断,当解压到压缩炸弹时,导致服务器ddos
3.13.2修复
Cookie[] cookies = request.getCookies();
String userRole = null;
for (int i = 0; i < cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("role")) {
userRole = c.getValue();
}
}
6
我们首先检查文件的大小是否超过了预设的最大值(10MB)。如果超过了最大值,则直接跳过解压缩操作。否则,我们使用ZipInputStream来读取zip文件中的每个条目,并对每个条目进行处理。在处理每个条目时,我们可以检查条目的大小是否超过了预设的最大值。如果超过了最大值,则拒绝解压缩该条目。
3.14防止边界操作发生越界
3.14.1错误代码示范
Cookie[] cookies = request.getCookies();
String userRole = null;
for (int i = 0; i < cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("role")) {
userRole = c.getValue();
}
}
7
我们定义了一个长度为5的整型数组arr,然后使用一个循环将0到5的值依次赋给数组中的每个元素。然而,由于数组的长度只有5个元素,而循环的范围是从0到5,因此会发生越界访问。
3.14.2修复
Cookie[] cookies = request.getCookies();
String userRole = null;
for (int i = 0; i < cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("role")) {
userRole = c.getValue();
}
}
8
循环的范围从0到arr.length-1,确保不会越过数组的边界。
3.15session未失效
3.15.1错误代码示范
Cookie[] cookies = request.getCookies();
String userRole = null;
for (int i = 0; i < cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("role")) {
userRole = c.getValue();
}
}
9
上述代码示例设置setMaxInactivelnterval为负值,使会话保持无限期的活动状态,会话持续时间越长,攻击者危害用户账户的机会就越大。如果创建大量的会话,较长的会话超时时间还会阻止系统释放内存,可能导致拒绝服务。
3.15.2修复
@WebServlet("/StoreUserInfo")
public class StoreUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 设置用户ID和角色信息
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
// 输出提示信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID和角色信息已存储到会话中");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
0
这个示例中,当用户登录成功后,将用户信息存入session,并设置了session的过期时间为30分钟。如果用户在30分钟内没有任何操作(如点击、提交表单等),session就会自动失效,导致用户被强制退出登录状态。
3.16 sql注入
3.16.1错误代码示范
@WebServlet("/StoreUserInfo")
public class StoreUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 设置用户ID和角色信息
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
// 输出提示信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID和角色信息已存储到会话中");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
1
在这个示例中,我们使用了MyBatis的注解方式来定义一个登录方法。但是,我们使用了${}来代替#{},这会导致SQL注入攻击。
3.16.2修复
@WebServlet("/StoreUserInfo")
public class StoreUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 设置用户ID和角色信息
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
// 输出提示信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID和角色信息已存储到会话中");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
2
在这个示例中,我们使用了#{}占位符来代替参数,然后在执行查询之前,将参数值绑定到占位符上。这样,即使用户输入了恶意SQL语句,也不会被执行,从而避免了SQL注入攻击。
3.17存储/反射型跨站脚本
3.17.1错误代码示范
@WebServlet("/StoreUserInfo")
public class StoreUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 设置用户ID和角色信息
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
// 输出提示信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID和角色信息已存储到会话中");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
3
在这个示例中,我们没有对用户输入的用户名进行任何过滤或转义,直接将其输出到页面上。这样,如果用户输入了恶意的JavaScript代码,就会导致XSS攻击。例如,用户可以输入<script>alert('XSS')</script>,这个代码会被浏览器执行,弹出一个警告框。
3.17.2修复
@WebServlet("/StoreUserInfo")
public class StoreUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 设置用户ID和角色信息
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
// 输出提示信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID和角色信息已存储到会话中");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
4
为了防止XSS攻击,我们需要对用户输入的数据进行过滤和转义。可以使用一些开源的库,如OWASP Java Encoder、Jsoup等来实现。以上使用Jsoup库过滤和转义用户输入的数据,Whitelist.basic()表示只允许基本的HTML标签和属性,不允许任何JavaScript代码。这样就可以防止XSS攻击。
推荐使用全局过滤器进行过滤,防止某接口被遗漏,以下是一个简单的Java过滤器示例,用于预防XSS攻击:
@WebServlet("/StoreUserInfo")
public class StoreUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 设置用户ID和角色信息
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
// 输出提示信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID和角色信息已存储到会话中");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
5
要使用这个过滤器,需要在web.xml文件中进行配置:
@WebServlet("/StoreUserInfo")
public class StoreUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 设置用户ID和角色信息
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
// 输出提示信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID和角色信息已存储到会话中");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
6
这样,所有的请求都会经过这个过滤器进行处理,从而预防XSS攻击。
3.18目录穿越/任意文件读取
3.18.1错误代码示范
@WebServlet("/StoreUserInfo")
public class StoreUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 设置用户ID和角色信息
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
// 输出提示信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID和角色信息已存储到会话中");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
7
攻击者可能提供类似下面的输入:
@WebServlet("/StoreUserInfo")
public class StoreUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 设置用户ID和角色信息
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
// 输出提示信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID和角色信息已存储到会话中");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
8
程序假定路径是有效的,因为它是以/safe_dir/开头的,但是../将导致程序删除important.dat文件的父目录。
3.18.2修复
@WebServlet("/StoreUserInfo")
public class StoreUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 设置用户ID和角色信息
session.setAttribute("userId", "12345");
session.setAttribute("role", "admin");
// 输出提示信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID和角色信息已存储到会话中");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
9
调用normalize()方法对其进行规范化,消除了..和.,防止了路径遍历
3.19文件上传
传统的文件上传无非是校验文件后缀名、校验文件内容、大小,下面这段代码没有对上传做任何校验
3.19.1错误代码示范
@WebServlet("/GetUserInfo")
public class GetUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 从会话中获取用户ID和角色信息
String userId = (String) session.getAttribute("userId");
String role = (String) session.getAttribute("role");
// 输出用户ID和角色信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID: " + userId);
response.getWriter().println("角色:" + role);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
0
我们使用了Java Servlet技术创建了一个简单的Web应用,其中有一个文件上传的接口。攻击者可以通过这个接口上传恶意文件,从而造成文件上传漏洞。为了防止这种漏洞,我们需要对上传的文件进行严格的验证和过滤。
3.19.2修复
同时做到以下两点,则无上传漏洞
1、限制上传文件的类型和大小。可以通过检查上传文件的MIME类型和大小来限制上传的文件类型和大小。例如,只允许上传图片文件,并且文件大小不能超过1MB。
@WebServlet("/GetUserInfo")
public class GetUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 从会话中获取用户ID和角色信息
String userId = (String) session.getAttribute("userId");
String role = (String) session.getAttribute("role");
// 输出用户ID和角色信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID: " + userId);
response.getWriter().println("角色:" + role);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
1
这段代码首先获取上传文件的类型、大小和名称,然后对文件类型、大小和后缀名进行校验。如果校验通过,将文件保存到服务器上。
2、对上传文件进行重命名。为了防止攻击者通过上传恶意文件来覆盖服务器上的其他文件,可以对上传的文件进行重命名。例如,将上传的文件名加上时间戳或者随机字符串。
@WebServlet("/GetUserInfo")
public class GetUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 从会话中获取用户ID和角色信息
String userId = (String) session.getAttribute("userId");
String role = (String) session.getAttribute("role");
// 输出用户ID和角色信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID: " + userId);
response.getWriter().println("角色:" + role);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
2
3.20 ognl表达式注入
OGNL表达式注入的核心在于将恶意OGNL语句传递给解析OGNL的函数,如Ognl.getValue()。如果这些函数中的参数可控,并且应用程序未对输入进行适当的验证和清洗,攻击者就能通过构造恶意的OGNL表达式来控制执行流程,从而实现远程代码执行或其他恶意操作。
3.20.1错误代码示范
@WebServlet("/GetUserInfo")
public class GetUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 从会话中获取用户ID和角色信息
String userId = (String) session.getAttribute("userId");
String role = (String) session.getAttribute("role");
// 输出用户ID和角色信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID: " + userId);
response.getWriter().println("角色:" + role);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
3
expression1 假使是外部用户可控的参数,那么Ognl.getValue在处理对象属性的时候,就会执行命令
3.20.2修复
@WebServlet("/GetUserInfo")
public class GetUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 从会话中获取用户ID和角色信息
String userId = (String) session.getAttribute("userId");
String role = (String) session.getAttribute("role");
// 输出用户ID和角色信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID: " + userId);
response.getWriter().println("角色:" + role);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
4
在危险参数expression1到达Ognl.getValue之前使用ConSpeChar对输入进行校验。
3.21url重定向
3.21.1错误代码示范
@WebServlet("/GetUserInfo")
public class GetUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 从会话中获取用户ID和角色信息
String userId = (String) session.getAttribute("userId");
String role = (String) session.getAttribute("role");
// 输出用户ID和角色信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID: " + userId);
response.getWriter().println("角色:" + role);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
5
获取到url参数后,直接跳转,造成url重定向
3.21.2修复
@WebServlet("/GetUserInfo")
public class GetUserInfo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 从会话中获取用户ID和角色信息
String userId = (String) session.getAttribute("userId");
String role = (String) session.getAttribute("role");
// 输出用户ID和角色信息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("用户ID: " + userId);
response.getWriter().println("角色:" + role);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
6
先取host域名或ip,在使用白名单进行匹配,如果不是白名单域名,则跳转失败。
渗透测试工具箱
魔改开发的一些工具
社区中举办的活动列表
免责声明:由于传播、利用本公众号鹏组安全所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号鹏组及作者不为此承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,我们会立即删除并致歉。谢谢!
文章来源:https://forum.butian.net/share/4036
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...