📚 网络安全 Kali 学习系列
模块 0 基础阶段 ✅ 已完成
01-18 Linux系统 → 计算机网络 → 密码学 → Python/Bash/PHP/JS → Docker
模块 1 信息收集与侦察 ✅ 已完成
19 nmap → 20 masscan → 21 指纹识别 → 22 漏洞扫描与 Web 基础
模块 2 Web 应用渗透 ✅ 已完成
23 sqlmap → 24 XSS → 25 CSRF/上传 → 26 命令执行 → 27 文件包含/SSRF → 28 反序列化 → 29 XXE → 30 越权 → 31 WAF
模块 3 代码审计(白盒分析阶段)
✓ 32 代码审计入门:从黑盒到白盒的思维转换
✓ 33 Node.js 原型链污染:原理与利用
34 PHP 代码审计实战:从危险函数到利用链(当前篇)
35 Java 代码审计:SpEL 与 JNDI 注入(预告)
模块 4-5 后续持续更新
导读
第 32 篇建立了代码审计的方法论框架,第 33 篇深入了 Node.js 的原型链污染。本篇把白盒审计的镜头对准 PHP——这门语言贡献了 Web 安全史上最多的漏洞类型。SQL 注入、文件上传、命令执行、反序列化,每一种你都已在模块 2 中从黑盒视角利用过,现在换个方向:打开源码,从代码层面找到这些漏洞的根源。
读完本篇你将能:搭建 PHP 代码审计环境并掌握从 grep 到 Semgrep 的扫描流程、识别 PHP 项目中的 SQL 注入审计特征(二次注入与 ORDER BY 注入)、从白盒角度审计文件上传漏洞(白名单绕过与条件竞争)、追踪 escapeshellarg() 的安全边界与绕过方式、定位 unserialize() 的调用点并追踪 POP 链起点。
目录
1. 审计环境搭建与流程总览
2. 危险函数搜索实战:grep + Semgrep 扫描
3. SQL 注入审计:二次注入与 ORDER BY 注入
4. 文件上传审计:白名单绕过与条件竞争
5. 命令执行审计:escapeshellarg 的安全边界
6. unserialize 审计:POP 链起点追踪
7. 分级练习

1. 审计环境搭建与流程总览

PHP 代码审计不需要复杂的工具链——一个能高亮 PHP 语法的编辑器加命令行工具就能开始。但为了让审计过程更高效,建议搭建以下三层环境。

层级 工具 用途
扫描层 grep / ripgrep + Semgrep 批量定位危险函数和漏洞模式
阅读层 VS Code + PHP 插件 / Vim + tagbar 跳转定义、追踪函数调用链
验证层 Docker + DVWA / Vulhub 复现漏洞确认可利用性

审计流程分四步走:扫描定位 → 人工确认 → 利用验证 → 修复建议。第一步用 grep 或 Semgrep 扫出所有危险函数调用点;第二步逐个人工检查参数来源是否可控;第三步在靶场环境中构造 Payload 验证漏洞是否真实可利用;第四步给出修复方案。本篇按这个流程逐步展开。

Shell · 审计环境快速搭建
# 1. 安装 ripgrep(比 grep 快 10 倍)
sudo apt install ripgrep
 
# 2. 安装 Semgrep(pip 安装最简单)
pip3 install semgrep
 
# 3. 拉取 DVWA 靶场用于验证
docker run -d -p 8080:80 --name dvwa vulnerables/web-dvwa
 
# 4. 获取待审计的 PHP 源码
git clone https://github.com/cispay/test-audit-php.git audit-target

环境就绪后,下一章开始实战扫描。

2. 危险函数搜索实战:grep + Semgrep 扫描

PHP 危险函数按漏洞类型分为五大类。审计的第一步不是逐行阅读源码,而是用搜索工具把所有危险函数调用点一次性扫出来,建立"待确认清单"。

漏洞类型 危险函数 grep 模式
代码执行 eval assert system exec rg "eval\|assert\|system\|exec"
命令执行 passthru shell_exec popen rg "passthru\|shell_exec\|popen"
反序列化 unserialize rg "unserialize"
文件包含 include require include_once rg "include\|require" --php
SQL 拼接 mysql_query mysqli_query rg "query.*\\\$_(GET\|POST)"

ripgrep 一键扫描

ripgrep(命令名 rg)比传统 grep 快数倍,默认递归、自动跳过 .git 目录和二进制文件。以下命令一次性扫出所有高危函数调用点,并附带行号和文件名。

Shell · ripgrep 一键扫描全部危险函数
1
2
3
4
5
6
7
8
# 一行扫出所有高危函数,-n 显示行号,-i 忽略大小写
rg -ni '\b(eval|assert|system|exec|passthru|shell_exec|popen|unserialize)\s*\(' .
 
# 输出示例:
config.php:12:  system($cmd);
upload.php:45: eval($_POST['code']);
db.php:78:  unserialize($cookieData);
utils.php:103: exec('convert ' . $filename);

扫描结果中每一行都是一个待确认点。但不是所有危险函数调用都有漏洞——如果参数是硬编码字符串(如 system('date')),就是安全的。只有参数来自用户输入($_GET、$_POST、$_COOKIE)或间接来自用户输入时,才需要深入分析。

💡 小贴士
PHP 中还有一类"间接危险函数"容易漏扫:call_user_func()、array_map()、usort()——它们的回调参数如果来自用户输入,等同于代码执行。扫描时加一条:rg -ni 'call_user_func|array_map|usort\|array_filter' .

3. SQL 注入审计:二次注入与 ORDER BY 注入

第 32 篇展示了"预处理语句误用"——拼接后送进 prepare() 的 SQL 注入。本篇深入两种更隐蔽的 SQL 注入模式,它们在黑盒测试中极难发现,但在白盒审计中一目了然。

二次注入:存储后才引爆

二次注入的核心特征是:用户输入第一次入库时被正确转义了(用了预处理语句),但第二次从数据库取出后直接拼入另一条 SQL 语句,没有再次转义。黑盒测试要构造两步 Payload 才能触发,而代码审计只需追踪数据从"入库"到"出库"到"二次拼接"的完整路径。

PHP · 二次注入:注册 + 修改密码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 第一步:注册时用户名安全入库(预处理)
$stmt = $pdo->prepare("INSERT INTO users(name) VALUES(?)");
$stmt->execute([$_POST['name']]);
// 攻击者注册用户名:admin'--
 
// 第二步:修改密码时取出用户名,直接拼接
$name = $_SESSION['name']; // "admin'--" 从数据库取出
// ⚠️ 直接拼入 SQL,未用预处理
$sql = "UPDATE users SET password='$newpass'";
$sql .= " WHERE name='$name'";
// 实际执行的 SQL:
// UPDATE users SET password='new' WHERE name='admin'--'
$pdo->query($sql);
// 结果:admin 的密码被改了,-- 后面的被注释
 
// 修复:所有 SQL 均使用预处理,包括取出的数据

审计要点:第 1-3 行入库用了预处理(安全),但第 7-10 行从数据库取出 $name 后直接拼接到 UPDATE 语句中。这种"入库安全但出库不安全"的模式是二次注入的典型特征。审计时搜索所有 query() 调用(非 prepare()),追踪其参数是否来自数据库查询结果。

ORDER BY 注入:预处理管不了的字段

预处理语句的 ? 占位符只能用于值的位置(WHERE 条件值、INSERT 值等),不能用于列名、表名或排序方向。当代码把用户输入直接放到 ORDER BY 后面时,预处理无法保护。

PHP · ORDER BY 注入
// 用户控制排序列和方向
$sort = $_GET['sort'] ?? 'id';
$order = $_GET['order'] ?? 'ASC';
 
// ⚠️ 预处理无法保护列名和方向
$sql = "SELECT * FROM products ORDER BY $sort $order";
$pdo->query($sql);
 
// 修复:用白名单严格限制可选项
$allowed = ['id', 'price', 'name'];
if (!in_array($sort, $allowed)) { $sort = 'id'; }
$order = ($order === 'DESC') ? 'DESC' : 'ASC';
⚠️ 常见错误
用 ORDER BY ? 加预处理来防注入

正确做法:列名和排序方向不能用预处理占位符,必须用白名单。对列名用 in_array($input, $allowed_columns) 严格校验,方向只允许 ASC 或 DESC 两个硬编码值。

第四章 文件上传审计:白名单绕过与条件竞争

文件上传是 Web 应用最危险的功能之一。一个上传校验不严格的接口,可以让攻击者直接获取 WebShell。审计文件上传功能时,关注点不是"有没有校验",而是"校验在哪一层、能不能被绕过"。

4.1 白名单绕过的五条路径

白名单本身比黑名单安全,但实现方式不对,白名单也能绕过。以下是审计中常见到的五种绕过路径:

路径1MIME 类型伪造 — 拦截 Content-Type 头判断类型,攻击者用 curl 伪造
路径2扩展名双写 — shell.phtml.php 被截取为 .php
路径3空字节截断 — shell.php%00.jpg,PHP < 5.3.4 截断后文件名为 .php
路径4.htaccess 绕过 — 上传 .htaccess 让 .jpg 文件被当做 PHP 解析
路径5大小写绕过 — shell.PhP 在大小写不敏感的系统上等效于 .php

先看一段典型的不安全上传代码,它在 MIME 类型和扩展名上都存在绕过可能:

PHP · 不安全的文件上传校验 vuln_upload.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 仅检查 MIME 类型 — 可被 curl 伪造
$allowed_types = ['image/jpeg', 'image/png'];
if (!in_array($_FILES['file']['type'], $allowed_types)) {
    die('文件类型不允许');
}
 
// 扩展名白名单 — 但用了 strtolower 后又末尾截取
$ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
$ext = strtolower($ext);
if (!in_array($ext, ['jpg', 'png'])) {
    die('扩展名不允许');
}
 
// 问题:未检查文件内容,也未重命名
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);

这段代码有两个致命缺陷。第一,$_FILES['file']['type'] 完全由客户端控制,用 curl -H "Content-Type: image/jpeg" 即可伪造。第二,文件名直接拼接保存,攻击者可上传 shell.php.jpg 然后利用服务器配置或 .htaccess 让其以 PHP 解析。

审计时需要问三个问题:MIME 类型校验是否依赖客户端可控数据?扩展名校验是否考虑了双扩展名和空字节?上传文件是否被重命名为随机名称?

💡 小贴士
审计文件上传时,搜索 move_uploaded_file 和 $_FILES 定位上传逻辑后,沿数据流回溯:文件名从哪来 → 经过了什么校验 → 最终保存路径是否可控。这条链路上任何一环可控都是漏洞。

4.2 条件竞争上传(TOCTOU)

条件竞争上传是文件审计中最容易被忽略的漏洞类型。它的核心逻辑是:先保存文件,再检查内容,最后删除恶意文件。在"保存"和"删除"之间的窗口期内,攻击者通过高并发请求访问该文件触发执行。

PHP · 条件竞争:先存后删 race_upload.php
1
2
3
4
5
6
7
8
9
10
11
12
13
// 步骤1:先保存文件到可访问目录
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);
 
// 步骤2:检查文件内容
$content = file_get_contents('uploads/' . $_FILES['file']['name']);
if (strpos($content, '<?php') !== false) {
    // 步骤3:发现恶意内容,删除
    unlink('uploads/' . $_FILES['file']['name']);
    die('恶意文件已删除');
}
 
// 窗口期:步骤1到步骤3之间,文件存在于可访问目录
// 攻击者用脚本并发访问 uploads/shell.php 触发执行

攻击者上传一个写入 WebShell 的 PHP 文件,内容为 <?php fputs(fopen('shell.php','w'),'<?php eval($_POST[cmd]);'); ?>。文件被保存的瞬间,攻击者用 Python 多线程脚本并发访问该文件,触发 PHP 执行写入持久化 WebShell,随后即使原文件被删除,WebShell 已经落地。

⚠️ 常见错误
上传后检查内容、发现恶意就删除,这样足够安全

正确做法:检查必须在保存前完成。文件先存到不可访问的临时目录,通过 getimagesize() 或 exif_imagetype() 验证文件头,校验通过后重命名为随机文件名并移动到可访问目录。不存在"先存后删"的窗口期。

4.3 安全上传的审计检查清单

审计文件上传功能时,按以下清单逐项核查:

1MIME 类型不依赖 $_FILES['type'],改用 finfo_file() 检测实际文件头
2扩展名白名单只允许 jpg/png/gif 等图片类型,禁止 php/php3/php5/phtml/pht
3上传文件重命名为 bin2hex(random_bytes(16)) 生成的随机名,不保留原始文件名
4校验在临时目录完成,通过后才移动到可访问目录,消除竞态窗口
5上传目录配置 Nginx/Apache 禁止执行 PHP,作为纵深防御

第五章 命令执行审计:从 escapeshellarg 到 RCE

PHP 中调用系统命令的函数有六个:exec、system、passthru、shell_exec、popen 和反引号操作符。审计时用 ripgrep 一次性搜索全部危险函数:

Shell · 搜索全部命令执行函数
1
2
3
4
5
6
# 在项目根目录搜索命令执行函数
rg '(exec|system|passthru|shell_exec|popen|proc_open)\s*\(' \
    --php --type-add'backtick:*.php' \
    -n --no-heading
 
# 输出示例:utils.php:42: exec("convert " . $filename . " out.png", $output);

5.1 命令注入 vs 参数注入

审计命令执行漏洞时,要区分两种不同的攻击方式。命令注入是攻击者在命令行中插入管道符或分号,执行全新命令;参数注入是攻击者在合法命令的参数位置注入额外选项,改变命令行为。

PHP · 命令注入与参数注入对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 命令注入:用户输入 ; 或 | 接续新命令
$ip = $_GET['ip']; // 8.8.8.8; cat /etc/passwd
system("ping -c 4 " . $ip);
// 实际执行:ping -c 4 8.8.8.8; cat /etc/passwd
 
// 参数注入:用户输入额外选项改变命令行为
$file = $_GET['name']; // --output=/tmp/shell.php
system("tar czf archive.tar.gz " . $file);
 
// escapeshellarg 防命令注入但不防参数注入
$ip = escapeshellarg($_GET['ip']);
system("ping -c 4 " . $ip);
// 输入 8.8.8.8; cat /etc/passwd → 被转义为 '8.8.8.8; cat /etc/passwd'
// 整体被当做一个参数传给 ping,命令注入失效
 
// 但 --output=/tmp/shell.php 仍被当做一个"参数"传给 tar
// escapeshellarg 不阻止以 - 开头的参数注入

关键结论:escapeshellarg() 能阻止管道符注入,但无法阻止参数注入。如果命令本身有危险参数(如 tar 的 --to-command 或 find 的 -exec),即使转义了输入仍可被利用。

5.2 escapeshellarg 的安全边界

审计中遇到 escapeshellarg() 不能直接判定安全。该函数在单引号外包裹输入,并转义内部单引号。但有两个已知边界问题:

边界1:多字节字符注入
在 LC_ALL=zh_CN.UTF-8 环境下,精心构造的多字节输入可"吃掉"转义单引号,PHP < 8.1 的旧版本受影响
边界2:escapeshellarg + escapeshellcmd 混用
两个函数同时使用反而会引入漏洞:escapeshellcmd 会破坏 escapeshellarg 的引号保护,导致注入
⚠️ 常见错误
用 escapeshellarg() 转义后拼接到命令中,再套一层 escapeshellcmd() 双重保险

正确做法:二选一,不要混用。escapeshellarg() 用于整个参数是用户输入的场景;escapeshellcmd() 用于用户输入只是命令的一部分时。最佳方案是用 proc_open() 传数组参数,完全绕过 shell 解析。

5.3 proc_open:最安全的命令调用方式

当业务必须调用系统命令时,proc_open() 以数组方式传参,不经过 shell 解析,从根源消除注入:

PHP · proc_open 数组传参 safe_exec.php
1
2
3
4
5
6
7
8
9
10
11
12
// 用户输入作为数组元素,不经 shell 解析
$ip = $_GET['ip'];
$desc = [
    ['pipe', 'r'],
    ['pipe', 'w'],
];
$proc = proc_open(
    ['ping', '-c', '4', $ip], // 数组,不经 shell
    $desc, $pipes
);
 
// 即使 $ip = "8.8.8.8; rm -rf /" 也只被当做 ping 的参数
💡 小贴士
命令执行审计的核心判断:用户输入是否出现在命令字符串中?如果是,检查是否用了 escapeshellarg;如果用了,再检查是否同时用了 escapeshellcmd;如果同时用了,标记为漏洞。最优解是搜索是否存在 proc_open 并建议迁移。

第六章 unserialize 审计:POP 链起点追踪

PHP 反序列化漏洞的核心不是 unserialize 函数本身,而是它触发的魔术方法调用链。审计时首先要定位 unserialize 的调用点,然后追踪输入是否可控,最后沿魔术方法链找到可利用的"终点"。

6.1 三大魔术方法与 POP 链构造

POP(Property Oriented Programming)链的构造依赖于 PHP 的魔术方法自动调用机制。审计时重点关注三个起点方法:

__wakeup()
unserialize() 时自动调用,最直接的入口。如果 __wakeup 中调用了危险方法或设置了属性,可作为链首
__destruct()
对象销毁时自动调用。即使 unserialize 时 __wakeup 被绕过,脚本结束时 __destruct 仍会执行,是最常用的触发点
__toString()
对象被当做字符串使用时调用。常作为中间环节,在 __destruct 中将属性对象当做字符串处理时触发

下面是一段典型的含 POP 链的代码,审计时需要从终点回溯到起点:

PHP · POP 链:__destruct → __toString → eval pop_chain.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 中间环节:Show 类有 __toString
class Show {
    public $cmd;
    public function __toString() {
        eval($this->cmd); // 终点:代码执行
        return '';
    }
}
 
// 起点:Destroyer 类有 __destruct
class Destroyer {
    public $obj;
    public function __destruct() {
        // 将属性当做字符串 → 触发 __toString
        echo $this->obj;
    }
}
 
// 漏洞入口:用户可控数据传入 unserialize
$data = $_COOKIE['session'];
$obj = unserialize($data);
 
// 脚本结束时 __destruct 自动调用 → echo $this->obj
// 若 obj 是 Show 对象 → 触发 __toString → eval($cmd)

攻击者构造的 payload 是一个序列化的 Destroyer 对象,其 obj 属性设为 Show 对象,Show 的 cmd 属性设为要执行的 PHP 代码。整个链路:unserialize 创建对象 → 脚本结束触发 __destruct → echo $this->obj 触发 __toString → eval 执行代码。

审计这类型漏洞的步骤是逆向的:先用 grep 搜索 eval|system|exec|include 等终点函数,确认它们在魔术方法中,然后回溯查找哪些类的 __destruct 或 __wakeup 会调用到这些方法,最后确认 unserialize 的输入是否可控。

6.2 phar 反序列化:不调用 unserialize 的漏洞

phar 反序列化是 PHP 中最隐蔽的漏洞之一。当 phar.readonly = Off 时,攻击者可以构造恶意 phar 文件,通过文件操作函数(如 file_exists、is_file、filesize 等)触发反序列化,无需代码中显式调用 unserialize。

PHP · phar 反序列化触发
1
2
3
4
5
6
7
8
9
10
11
12
// 看似无害的文件存在检查
$path = $_GET['file']; // phar://evil.phar
if (file_exists($path)) {
    echo '文件存在';
}
 
// file_exists 处理 phar:// 协议时
// 自动解析 phar 的 manifest 元数据
// manifest 中的序列化数据被 unserialize
// 无需代码中显式调用 unserialize()
 
// 审计要点:搜索所有文件操作函数 + phar 协议

审计 phar 反序列化时,搜索所有文件系统函数(file_exists、is_dir、filesize、file_get_contents、fopen 等共 30+ 个)是否接受用户输入,并检查输入是否可能以 phar:// 开头。修复方案是对路径输入做 realpath() 校验和协议白名单过滤。

💡 小贴士
PHP 8.0 中 __wakeup 的绕过已被修复(不再可通过设置属性数量不一致来跳过),但 __destruct 和 phar 反序列化仍然有效。审计 PHP 8 项目时,重点搜索 __destruct 而非 __wakeup。

6.3 Semgrep 自动化反序列化规则

手动审计大型项目时,用 Semgrep 自定义规则批量搜索 unserialize + 可控输入的模式:

YAML · Semgrep 反序列化检测规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rules:
  - id: php-unserialize-user-input
    pattern: unserialize($X)
    pattern-inside: |
      $INPUT = $_REQUEST[...];
      ...
      unserialize($INPUT);
    message: "用户可控数据传入 unserialize,存在反序列化风险"
    languages: [php]
    severity: ERROR
  - id: php-phar-deserialization
    pattern: file_exists($_GET[...])
    message: "file_exists 接受用户输入,可能触发 phar 反序列化"
    languages: [php]
    severity: WARNING

运行 semgrep --config rules.yml --php ./src/ 即可自动标记所有用户可控输入流入 unserialize 和文件操作函数的位置,大幅减少人工搜索工作量。

第七章 分级练习

以下练习按难度递进,建议在本地搭建 DVWA 或自行编写漏洞代码进行实操。每个练习标注了涉及的知识点和审计思路。

🟢 基础练习:危险函数定位

练习 1:用 ripgrep 扫描项目中的全部危险函数
任务:在任意 PHP 项目(可用 DVWA 源码)中,编写一条 ripgrep 命令,一次性搜索以下六类危险函数:
① 命令执行:exec、system、passthru、shell_exec、popen
② 代码执行:eval、assert、create_function
③ 文件包含:include、require(带变量参数的)
④ 文件操作:move_uploaded_file、file_get_contents(带变量参数的)
⑤ 反序列化:unserialize
⑥ SQL 拼接:mysqli_query、$pdo->query(带字符串拼接的)

验证标准:每条结果都带行号和文件名,手动检查前 5 条命中,判断是否真正可利用。

审计思路:先广度搜索定位,再逐个深度分析。不是所有危险函数调用都是漏洞,关键看参数是否可控。

🟡 进阶练习:构造 POP 链

练习 2:手工构造反序列化 payload
任务:根据以下代码片段,构造一个可执行 phpinfo() 的序列化 payload:

代码结构:Class A 有 __destruct() 调用 $this->b->run();Class B 有 __call() 魔术方法转发到 eval($this->code);入口是 unserialize($_POST['data'])

步骤:
1. 从终点 eval 回溯:需要触发 B 的 __call
2. __call 由"调用不存在的方法"触发,A 的 __destruct 调用 $this->b->run(),若 b 是 B 的实例且 B 没有 run 方法 → 触发 __call
3. 构造:A 对象的 b 属性 = B 对象,B 的 code 属性 = "phpinfo()"
4. 用 php -r "echo serialize(...);" 生成 payload

验证标准:payload 提交后页面输出 phpinfo 信息。

🔴 高级练习:编写 Semgrep 自定义规则

练习 3:编写 Semgrep 规则检测二次注入
任务:编写一条 Semgrep 规则,检测"从数据库取出数据后直接拼入 SQL"的二次注入模式。

提示:
① 模式匹配 $X = $pdo->query("SELECT ...")->fetch() 后接 $pdo->query("... $X ...")
② 使用 pattern-inside 和 metavariable-relationship 关联两个语句
③ message 字段说明风险:数据库取出的数据可能包含 SQL 注入 payload

验证标准:在含二次注入漏洞的测试代码上运行,规则能命中且不误报安全代码。思考:为什么预处理不能防止二次注入?

知识回顾

危险函数六大类 ripgrep 搜索定位 Semgrep 自定义规则 二次注入原理 ORDER BY 白名单 文件上传五条绕过 条件竞争 TOCTOU escapeshellarg 边界 proc_open 数组传参 POP 链构造 phar 反序列化 魔术方法回溯审计
下一篇预告
📚 网络安全 Kali 学习系列 · 第 35 篇
Java 代码审计:SpEL 注入与 Fastjson 反序列化
从 PHP 审计过渡到 Java 生态,聚焦 Spring Boot 项目中最常见的两类漏洞:SpEL 表达式注入和 Fastjson 反序列化链,对比两种语言的审计思路差异。

网络安全 Kali 学习系列 · 模块 3 代码审计 · 第 34 篇

学安全,为守护 · 用安全,为建设