news 2026/7/5 4:23:52

PHP开发实战:SQL注入攻防全解析与安全编码实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP开发实战:SQL注入攻防全解析与安全编码实践

1. 项目概述:为什么PHP开发者必须直面SQL注入

如果你正在学习PHP,或者已经用它写过几个带数据库的网站,那么“SQL注入”这个词你一定不陌生。它就像一个幽灵,在无数新手开发者的代码里游荡,随时可能让辛辛苦苦做的网站变成攻击者的“后花园”。我见过太多因为一个简单的查询语句没处理好,导致整个用户数据库被拖走,甚至服务器被拿下的案例。所以,当我们要从“入门”迈向“实战”时,安全开发是必须跨过去的一道坎,而SQL注入攻防,就是这门课里最核心、也最惊心动魄的一章。

这个模块的目标很明确:我们不只讲那些枯燥的“不要用字符串拼接”的理论,而是要带你亲手“攻击”一个脆弱的网站,看看漏洞是如何被利用的;然后再亲手把它加固成“固若金汤”的堡垒,理解每一种防御手段背后的原理。你会使用像DVWA、Pikachu这样的靶场,这些都是安全圈内公认的练手环境。通过从攻击者视角理解漏洞,你才能真正以防御者思维写出安全的代码。无论你是想成为一名合格的PHP后端工程师,还是对CTF(Capture The Flag)网络安全竞赛感兴趣,这部分实战经验都至关重要。

2. 漏洞原理深度拆解:SQL注入究竟是如何发生的?

要防御,必须先透彻理解攻击。SQL注入的本质,是“程序代码”与“用户数据”的边界被模糊了。攻击者将精心构造的数据(恶意SQL代码片段)作为输入提交给应用程序,而应用程序未加验证或过滤,便将其直接拼接到要执行的SQL查询语句中,导致数据库引擎执行了非预期的命令。

2.1 核心漏洞模型:一个万能密码的诞生

我们来看一个最经典的漏洞代码片段,这可能是很多PHP入门教程里会出现的写法:

$username = $_POST['username']; $password = $_POST['password']; $sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"; $result = mysqli_query($conn, $sql);

假设正常的用户输入是username='admin'password='123456',那么拼接后的SQL语句是:

SELECT * FROM users WHERE username = 'admin' AND password = '123456'

这没问题。但如果攻击者在密码框里输入的不是密码,而是' OR '1'='1呢?拼接后的语句就变成了:

SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1'

由于'1'='1'这个条件永远为真(True),整个WHERE子句的逻辑就变成了:查找用户名为admin,并且(密码为空 或者 1等于1)。OR后面的条件恒真,导致整个查询条件被绕过。数据库会返回users表里第一条用户名为admin的记录(甚至可能返回所有用户),攻击者就这样在没有正确密码的情况下“登录”成功了。这就是最常见的“万能密码”攻击。

2.2 注入类型与攻击手法演进

SQL注入远不止绕过登录这么简单。根据应用程序处理输入的方式和数据库报错信息的回显,攻击手法多种多样:

1. 基于报错的注入(Error-Based)这是新手攻击者最“喜欢”的类型。当应用程序将数据库的错误信息直接显示给用户时,攻击者就获得了宝贵的信息来源。例如,输入' AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT(database(),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a) --这类精心构造的Payload,会触发数据库的重复键错误,并将当前数据库名等信息包含在错误信息中返回。攻击者可以像“挤牙膏”一样,一步步获取数据库名、表名、列名,最终拖取数据。

2. 联合查询注入(Union-Based)这是信息获取最直接的方式。前提是页面会显示SQL查询的结果。攻击者利用UNION操作符,将恶意查询的结果“附加”到原始查询结果之后,一起显示在页面上。关键步骤是:

  • 判断列数:通过ORDER BY 5试探,直到报错,来确定原始查询返回的列数。
  • 判断显示位:使用UNION SELECT 1,2,3...来确定页面上哪些位置会显示我们查询的数据。
  • 窃取信息:将显示位替换为如SELECT database(), user(), version()等查询,直接获取系统信息。

3. 布尔盲注(Boolean Blind)当页面没有明显回显,也不会显示具体错误信息,但会根据查询条件返回“正常页面”或“错误页面”(如404、登录失败)时,就适用布尔盲注。攻击者通过构造真/假条件,观察页面反应的差异,像“猜谜”一样一位一位地获取数据。例如:?id=1 AND SUBSTRING(database(),1,1)='a',如果页面正常,说明数据库名第一个字母是‘a’;否则就换下一个字母猜。这个过程非常缓慢,但自动化工具(如sqlmap)可以高效完成。

4. 时间盲注(Time-Based Blind)这是最隐蔽的一种。页面无论查询真假,返回的内容都一样。此时,攻击者利用数据库的延时函数(如MySQL的SLEEP()、PostgreSQL的pg_sleep()),通过观察页面响应时间是否延长来判断条件真假。例如:?id=1 AND IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0)。如果页面响应延迟了5秒,就说明第一个字母是‘a’。这种注入对自动化工具友好,但难以被传统的Web防火墙(WAF)基于内容匹配的规则检测到。

注意:以上所有攻击手法的演示和学习,必须在你自己搭建的本地靶场(如DVWA)或获得明确授权的测试环境中进行。未经授权对任何线上网站进行测试都是非法行为。

3. 实战环境搭建与靶场配置

“纸上得来终觉浅,绝知此事要躬行。” 安全攻防尤其如此。我们需要一个安全的、合法的沙箱环境来练习。

3.1 环境选择:一体化方案 vs 自定义搭建

对于初学者,我强烈推荐使用一体化集成环境来搭建靶场,这能避免你把大量时间浪费在环境配置的坑里。

  • PHPStudy(Windows):一个集成了Apache/Nginx、PHP、MySQL的软件包,一键安装启动。非常适合快速在Windows上搭建环境。
  • XAMPP/MAMP(跨平台/ Mac):同样是一键式的本地服务器解决方案。
  • Docker(推荐给有一定基础的学习者):这是目前最“干净”和可复现的方式。你可以搜索vulhubdocker-compose编写的DVWA镜像,一条命令就能启动一个包含完整漏洞环境的容器,用完即删,不影响宿主机。

这里以PHPStudy + DVWA为例,给出快速搭建步骤:

  1. 从官网下载并安装PHPStudy,启动Apache和MySQL服务。
  2. 从GitHub下载DVWA(Damn Vulnerable Web Application)的源码。
  3. 将DVWA文件夹解压到PHPStudy的WWW根目录下。
  4. 浏览器访问http://localhost/DVWA,根据安装向导进行配置。主要步骤是:
    • 复制config/config.inc.php.distconfig/config.inc.php
    • 修改该文件中的数据库密码,与PHPStudy中MySQL的root密码(默认常为root)一致。
    • 在DVWA安装页面点击Create / Reset Database按钮,完成数据库初始化。
  5. 使用默认账号admin/password登录,在左侧DVWA Security页面中,将安全等级设置为Low,这样漏洞最明显,便于我们学习攻击。

3.2 靶场初探:DVWA SQL注入模块解析

成功登录DVWA后,我们重点看SQL Injection这个模块。在Low安全级别下,它的后端代码几乎就是我们前面提到的漏洞模型的翻版:

$id = $_REQUEST['id']; $getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id'"; $result = mysqli_query($GLOBALS["___mysqli_ston"], $getid);

代码直接使用了$_REQUEST['id']获取用户输入,并毫无过滤地拼接进了SQL字符串。这就是我们绝佳的“攻击演练场”。你可以尝试在输入框里输入1' OR '1'='1,看看返回什么?很可能不再是单个用户信息,而是整个用户列表。这就是联合查询注入发挥作用的地方。

实操心得:在搭建环境时,最常见的坑是数据库连接失败。请务必检查:

  1. PHPStudy中的MySQL服务是否真的启动(绿灯)。
  2. DVWA配置文件config.inc.php中的$_DVWA[ 'db_password' ]是否与PHPStudy的MySQL密码一致。
  3. 如果使用Docker,确保容器端口(如80)映射到了宿主机的某个未被占用的端口(如8080)。

4. 攻击者视角:手把手进行SQL注入实战

现在,我们化身“攻击者”,以DVWA Low级别的SQL注入为例,进行一次完整的手工注入流程。目的是获取数据库中的敏感信息。

4.1 第一步:信息探测与漏洞确认

首先,在输入框输入一个单引号'并提交。如果页面返回了数据库错误信息(如You have an error in your SQL syntax...),那么基本可以确认存在SQL注入漏洞,并且是错误信息回显的,这为我们后续利用提供了便利。

接着,我们尝试构造一个永真条件来确认漏洞可利用性。输入1' OR '1'='1。如果页面返回了所有用户的数据,而不是仅ID为1的用户,那么漏洞确认无误。

4.2 第二步:判断列数与显示位

我们的目标是使用UNION查询,但必须先知道原始查询SELECT first_name, last_name FROM users ...返回了几列数据。我们使用ORDER BY子句来探测:

  • 输入1' ORDER BY 1 ----是SQL注释符,用于注释掉后面的语句,避免语法错误)。页面正常。
  • 输入1' ORDER BY 2 --。页面正常。
  • 输入1' ORDER BY 3 --。页面报错或返回空。 这说明原始查询只返回2列数据。

接下来,我们需要找到这2列数据在页面上的哪个位置被显示出来。我们构造一个联合查询,让联合查询的结果显示数字: 输入:1' UNION SELECT 1,2 --观察页面。通常,原本显示first_namelast_name的地方,现在会分别被数字12所替代。这就告诉我们,第一个显示位对应SELECT后的第一列,第二个显示位对应第二列。

4.3 第三步:利用显示位窃取系统信息

现在,我们可以把显示位替换成我们想查询的信息了。数据库有一系列内置函数和系统表(如information_schema)来存储元数据。

  1. 获取当前数据库名和用户: 输入:1' UNION SELECT database(), user() --页面可能会在相应位置显示类似dvwaroot@localhost的信息。

  2. 获取数据库中的所有表名information_schema.tables表存储了所有表的信息。我们查询属于当前数据库的表: 输入:1' UNION SELECT table_name, NULL FROM information_schema.tables WHERE table_schema=database() --(因为只有两个显示位,我们用NULL占位,也可以使用group_concat(table_name)将所有表名合并到一个字段显示)。 在返回结果中,你很可能看到users,guestbook等表名。我们对users表特别感兴趣。

  3. 获取users表的所有列名information_schema.columns表存储了所有列的信息。 输入:1' UNION SELECT column_name, NULL FROM information_schema.columns WHERE table_schema=database() AND table_name='users' --返回的结果可能包括user_id,first_name,last_name,user,password,avatar等。其中userpassword是我们的终极目标。

  4. 拖取最终的用户名和密码哈希: 输入:1' UNION SELECT user, password FROM users --成功!你现在应该看到了所有用户的登录名和密码哈希值(通常是MD5加密后的字符串)。攻击者拿到这个哈希值后,可以通过彩虹表碰撞或在线解密网站,有很大概率还原出明文密码。

手工注入的体会:这个过程虽然繁琐,但能让你深刻理解每一步攻击的原理和数据库的结构。在真实的高强度攻击中,攻击者会使用sqlmap这样的自动化工具,将上述过程在几秒内完成。但作为开发者,只有亲手做过一遍,你才能对攻击链的每一个环节都了如指掌。

5. 防御者视角:构建多层防御体系

理解了攻击,防御就有了清晰的靶子。防御SQL注入的核心思想就一条:永远不要信任用户输入,严格区分代码与数据。我们需要构建一个从输入到执行的多层防御体系。

5.1 第一道防线:使用参数化查询(预编译语句)

这是唯一从根本上杜绝SQL注入的方法,必须作为所有数据库操作的首选。它的原理是将SQL语句的“结构”(模板)与“数据”(参数)分开发送数据库。数据库先对语句模板进行编译(确定语法、生成执行计划),然后再将用户输入的数据作为纯粹的“参数”传入。这样,即使参数中包含SQL关键字或特殊符号,也只会被当作普通字符串处理,而不会被解释为SQL代码的一部分。

在PHP中,我们使用PDO(PHP Data Objects)MySQLi扩展来支持参数化查询。

PDO示例:

// 1. 连接数据库(启用异常模式便于错误处理) $pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'username', 'password'); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 2. 准备SQL语句模板,使用命名占位符 :id $stmt = $pdo->prepare("SELECT first_name, last_name FROM users WHERE user_id = :id"); // 3. 绑定参数。这里将用户输入的 $id 变量绑定到 :id 占位符,并指定为整数类型(PDO::PARAM_INT) $stmt->bindParam(':id', $id, PDO::PARAM_INT); // 4. 执行查询 $id = $_GET['id']; // 假设从URL获取 $stmt->execute(); // 5. 获取结果 $results = $stmt->fetchAll(PDO::FETCH_ASSOC);

MySQLi示例(面向对象风格):

$mysqli = new mysqli('localhost', 'username', 'password', 'test'); $stmt = $mysqli->prepare("SELECT first_name, last_name FROM users WHERE user_id = ?"); $stmt->bind_param('i', $id); // 'i' 表示参数类型为整数 $id = $_GET['id']; $stmt->execute(); $result = $stmt->get_result(); $rows = $result->fetch_all(MYSQLI_ASSOC);

关键点bindParambind_param中的类型指定(如PDO::PARAM_INT,'i')至关重要。它告诉数据库驱动程序在传递前对数据进行强制类型转换,提供了另一层保护。对于字符串,使用PDO::PARAM_STR's'

5.2 第二道防线:严格的输入验证与过滤

参数化查询解决了“数据”部分的问题,但良好的安全习惯要求我们对输入本身也要有约束。输入验证的原则是:基于“白名单”而非“黑名单”

  • 类型检查:如果期望是整数,就用intval()filter_var($input, FILTER_VALIDATE_INT)进行强制转换和验证。
    $id = filter_var($_GET['id'], FILTER_VALIDATE_INT); if ($id === false) { // 不是有效的整数,记录日志并返回错误,终止后续流程 die('Invalid input'); }
  • 长度限制:对于用户名、邮箱等,设置合理的最大长度限制,防止超长字符串攻击。
  • 格式匹配:对于邮箱、URL、日期等,使用正则表达式或filter_var函数验证格式。
    $email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);

关于转义函数的误区:很多老教程会提到mysqli_real_escape_string()addslashes()。这些函数是对特殊字符(如引号)进行转义,使其变成普通字符。它们不能替代参数化查询!因为:

  1. 它们依赖于当前数据库连接的字符集,如果设置不当可能被绕过。
  2. 在复杂的查询(如WHERE column IN ($list))或数字型注入中,转义是无效或容易出错的。
  3. 开发者容易忘记使用,或者用错位置。因此,仅将转义函数视为在无法使用参数化查询时的最后一道应急措施,且必须与正确的字符集设置配合使用

5.3 第三道防线:最小权限原则与数据库加固

即使应用层代码完美无缺,我们也应为最坏情况(例如,存在未知的0day漏洞)做准备。

  1. 应用数据库账户权限最小化

    • 永远不要使用root或具有超级权限的账户连接Web应用数据库。
    • 创建一个专属的数据库用户,只授予其完成业务所必需的最小权限。例如,一个只需要读取用户信息的页面,连接账户就只给SELECT权限;不需要DELETEDROPFILE等危险权限。
    • 将数据库用户的操作限制在特定的数据库或表上。
  2. Web应用程序权限最小化

    • 运行PHP-FPM或Apache进程的用户(如www-data,nobody)应该是一个低权限用户。
    • 确保网站目录的文件权限设置正确,避免PHP文件可被任意写入。
  3. 数据库配置加固

    • 禁用或限制数据库的某些危险功能。例如,在MySQL中,可以通过启动参数--secure-file-priv限制LOAD DATA INFILESELECT ... INTO OUTFILE操作,防止文件读写漏洞。
    • 确保数据库服务本身不暴露在公网,只允许Web服务器内网IP访问。

5.4 第四道防线:纵深防御与监控

  1. Web应用防火墙(WAF):在应用服务器前部署WAF,如ModSecurity(开源),可以基于规则库拦截常见的SQL注入、XSS等攻击模式。它是一种有效的缓解措施,但不能替代安全的代码,因为攻击者可能构造出绕过WAF规则的Payload。
  2. 错误信息处理:务必在生产环境中关闭PHP和数据库的详细错误回显。将display_errors设置为Off,将错误记录到日志文件中,而不是展示给用户。自定义一个友好的错误页面。
  3. 安全编码规范与代码审计:在团队中推行安全编码规范,强制使用参数化查询。定期进行代码审计,或使用静态代码分析工具(如SonarQube, PHPStan的某些安全插件)来扫描项目中的潜在漏洞。
  4. 日志与监控:记录所有数据库查询的错误日志(注意不要记录密码等敏感信息)。监控异常的查询模式,例如短时间内大量复杂的联合查询、报错查询,这可能是自动化攻击工具正在扫描的迹象。

6. 进阶实战:DVWA中高级别注入与防御绕过思考

在DVWA中,将安全级别调到MediumHigh,你会发现注入点依然存在,但防御手段升级了,攻击方式也需要相应调整。这模拟了真实世界中开发者尝试修复,但修复不彻底的情况。

  • Medium级别:可能将$_GET换成了$_POST,或者使用了mysql_real_escape_string()进行转义。但对于数字型注入(如WHERE id = $id),转义是无效的,因为攻击者根本不需要闭合引号。攻击思路需要从字符型注入转向数字型注入,或者寻找其他未过滤的点(如HTTP头)。
  • High级别:可能使用了更严格的过滤,或者将用户输入限制在了下拉菜单中。这时可能需要结合前端的HTML源码分析,或者利用Cookie、User-Agent等HTTP头部的注入点进行攻击(这属于“二次注入”或“HTTP头注入”的范畴)。

分析这些不完整的防御,能让你更深刻地理解:安全是一个整体,任何一环的疏忽都可能导致前功尽弃。仅仅转义是不够的,必须结合正确的查询方式(参数化)和输入验证。

7. 自动化攻击工具sqlmap初探与防御启示

在实战中,攻击者很少手工注入。sqlmap是一个开源的自动化SQL注入检测与利用工具,功能极其强大。了解它的工作原理,对于防御者至关重要。

基本使用示例(仅用于本地靶场学习)

# 检测一个GET参数是否存在注入 sqlmap -u "http://localhost/DVWA/vulnerabilities/sqli/?id=1&Submit=Submit" --cookie="PHPSESSID=你的会话ID; security=low" # 获取当前数据库名 sqlmap -u "http://localhost/DVWA/vulnerabilities/sqli/?id=1" --cookie="..." --current-db # 获取指定数据库的所有表 sqlmap -u "http://localhost/DVWA/vulnerabilities/sqli/?id=1" --cookie="..." -D dvwa --tables # 获取指定表的所有列和数据 sqlmap -u "http://localhost/DVWA/vulnerabilities/sqli/?id=1" --cookie="..." -D dvwa -T users --dump

sqlmap会自动探测注入类型、数据库类型,并采用最优策略进行信息获取和数据拖取。

从sqlmap看防御sqlmap之所以强大,是因为它内置了数百种Payload和绕过技巧(如混淆、编码、等价函数替换)。这告诉我们:

  1. 黑名单过滤(WAF规则)永远会落后于攻击技术:依赖正则表达式过滤UNION,SELECT,SLEEP等关键词,很容易被UnIoN,SELSELECTECT, 或使用注释符/**/拆分的写法绕过。
  2. 强化根本性防御:正因为有如此多变的绕过手法,我们才更要坚持使用参数化查询这一根本方法。无论Payload如何变形,只要它作为“数据”传入预编译的语句,就无法改变语句的“结构”。
  3. 降低攻击面:除了修复代码,还应通过配置(如WAF)、监控和日志分析,增加攻击者的成本和风险,实现纵深防御。

8. 总结与持续学习路径

通过从攻击到防御的完整实战,你应该已经深刻体会到,SQL注入不是一个高深莫测的黑客技术,它源于开发中最常见的疏忽。防御它也不复杂,核心就是坚持使用参数化查询(预编译语句)

回顾一下构建“固若金汤”防御的要点:

  1. 首选PDO/MySQLi的参数化查询:这是你的“金钟罩”。
  2. 对所有输入进行严格的白名单验证:这是你的“护城河”。
  3. 遵循最小权限原则配置数据库:这是你的“保险柜”。
  4. 关闭错误回显,记录安全日志:这是你的“监控摄像头”。
  5. 保持框架和依赖库更新:很多现代PHP框架(如Laravel, Symfony)的ORM(Eloquent, Doctrine)已经默认使用了参数化查询,但你需要了解其原理,避免错误使用。

安全之路没有终点。在掌握了SQL注入之后,你应该继续探索Web安全的其他核心领域:

  • 跨站脚本(XSS):攻击者将恶意脚本注入到网页中,其他用户浏览时受害。
  • 跨站请求伪造(CSRF):诱骗用户在已登录的Web应用中执行非本意的操作。
  • 文件上传漏洞:上传的文件未被严格校验,导致恶意文件被执行。
  • 命令注入:与SQL注入类似,但发生在系统命令的拼接中。
  • 不安全的反序列化:在PHP中处理unserialize()时需要格外小心。

建议你继续在DVWA、Pikachu、WebGoat等靶场中练习这些漏洞。同时,关注OWASP(开放Web应用安全项目)每年发布的Top 10安全风险报告,这是Web安全领域的风向标。记住,安全是一种思维方式,它应该贯穿在你编写每一行代码的过程中。当你养成习惯,每次拼接字符串时都心里一紧,然后果断改用preparebindParam时,你就已经是一名具备安全意识的合格开发者了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/5 4:23:15

Ryujinx免费Switch模拟器:五分钟快速上手,畅玩4000+款Switch游戏

Ryujinx免费Switch模拟器:五分钟快速上手,畅玩4000款Switch游戏 【免费下载链接】Ryujinx 用 C# 编写的实验性 Nintendo Switch 模拟器 项目地址: https://gitcode.com/GitHub_Trending/ry/Ryujinx 想要在PC上体验任天堂Switch游戏的魅力吗&#…

作者头像 李华
网站建设 2026/7/5 4:21:56

五级Markdown目录模板

五级Markdown目录模板 一级标题 第一章 项目总览 二级标题 1.1 系统架构设计 三级标题 1.1.1 前端模块划分 四级标题 1.1.1.1 编辑器核心组件 五级标题 1.1.1.1.1 Markdown编辑模块 支持分栏编辑、实时预览、滚动同步功能,内置mermaid图表渲染、代码高亮插件。集成图…

作者头像 李华
网站建设 2026/7/5 4:21:08

SVGcode终极指南:3分钟学会免费在线图像矢量化转换

SVGcode终极指南:3分钟学会免费在线图像矢量化转换 【免费下载链接】SVGcode Convert color bitmap images to color SVG vector images. 项目地址: https://gitcode.com/gh_mirrors/sv/SVGcode 想要将普通的JPG、PNG图片转换成可无限缩放的矢量图形吗&#…

作者头像 李华
网站建设 2026/7/5 4:20:52

抖店订单利润低但有销量要不要继续卖商家怎么判断取舍

抖店订单利润低但有销量要不要继续卖?商家怎么判断取舍 有些抖店商品销量不错,但每单利润很低,甚至遇到售后就亏。商家这时候很纠结:继续卖怕白忙,不卖又舍不得流量。判断这类商品,不能只看销量&#xff0c…

作者头像 李华
网站建设 2026/7/5 4:20:22

应用框架架构设计实践 - 概述

我研究领域驱动设计已经近4年时间了,在这4年里,我从了解领域驱动设计的基本思想开始,系统地学习了与领域驱动设计相关的概念、开发模式以及应用系统架构风格,并将其运用在了实际的项目架构与开发中。在此之前,我一直被…

作者头像 李华
网站建设 2026/7/5 4:19:49

DayZ社区离线模组:5步打造完美单人末日生存体验

DayZ社区离线模组:5步打造完美单人末日生存体验 【免费下载链接】DayZCommunityOfflineMode A community made offline mod for DayZ Standalone 项目地址: https://gitcode.com/gh_mirrors/da/DayZCommunityOfflineMode DayZCommunityOfflineMode是一个由社…

作者头像 李华