前言

在Web安全领域,注入类漏洞一直是威胁系统安全的“重灾区”。其中,命令注入(Command Injection)和代码注入(Code Injection)因其能直接危害系统权限、破坏数据完整性,成为安全测试人员重点关注的对象。

命令注入允许攻击者通过可控输入执行系统终端命令,直接接管服务器;代码注入则能让攻击者注入恶意代码并执行,灵活性更高,危害范围更广。本文将从代码审计角度出发,解析两种漏洞的产生原理、关键代码特征、利用限制及检测要点,帮助安全测试人员快速识别并验证此类漏洞,为系统安全防护提供依据。

一、命令注入漏洞解析

命令注入的核心风险在于:程序将用户可控输入直接拼接为系统命令并执行,且未做严格过滤。以下从多语言视角结合代码实例分析其原理。

1. 危险函数识别

不同语言中存在直接执行系统命令的危险函数,是命令注入的高风险点:

  • Pythonexecevalos.systemos.openexecfile等函数可直接执行系统命令或代码,若参数包含用户输入且未过滤,极易引发漏洞。
  • JavaRuntime.getRuntime().exec()是执行系统命令的核心方法,若其参数由用户直接控制,可能导致任意命令执行。此外还要关注ProcessBuilder ProcessImpl (底层类)、如Apache Commons Exec、Guava的某些工具类

2. 典型漏洞代码分析(Java)

如下代码直接获取用户输入的cmd参数并通过Runtime.exec()执行,存在高危命令注入漏洞:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String cmd = req.getParameter("cmd"); // 用户可控输入,未过滤
    Process process = Runtime.getRuntime().exec(cmd); // 直接执行用户输入的命令
    InputStream in = process.getInputStream();
    // 读取命令执行结果并返回
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    byte[] b = new byte[1024];
    int i = -1;
    while ((i = in.read(b)) != -1) {
        byteArrayOutputStream.write(b, 0, i);
    }
    PrintWriter out = resp.getWriter();
    out.print(new String(byteArrayOutputStream.toByteArray()));
}

风险点:用户可通过cmd参数传入任意系统命令(如whoamiipconfig),直接获取服务器权限。

3. 命令注入的限制(以Java为例)

Java的Runtime.exec()对命令参数的处理机制可能限制注入的利用,需特别注意:

  • exec传入字符串类型命令时,会通过StringTokenizer分割空白字符,将拼接后的内容作为单个命令的参数,而非多条命令。

    例如,如下代码中用户输入www.baidu.com&ipconfig,本意是执行ping www.baidu.comipconfig两条命令,但实际会被当作ping的参数(www.baidu.com&ipconfig),导致注入失败:

    protected ByteArrayOutputStream ping(String url) throws IOException {
        // url为用户输入,拼接后作为ping命令的参数
        Process process = Runtime.getRuntime().exec("ping "+ url); 
        InputStream in = process.getInputStream();
        // 读取结果逻辑...
        return byteArrayOutputStream;
    }
    

可以使用www.baidu.com; ipconfig的形式突破限制

  • exec传入字符串数组(如exec(new String[]{"ping", url})),参数分割更严格,注入难度更高。

二、代码注入漏洞解析

代码注入与命令注入类似,但更灵活:攻击者注入的是可执行代码(如Java代码),而非系统命令。其核心是程序将用户输入作为代码执行,Java反射机制是常见触发点。

1. 危险函数识别

# 动态类加载与反射
Class.forName
ClassLoader.loadClass
Method.invoke()
Constructor.newInstance()

# 脚本引擎
javax.script.ScriptEngine

# JNDI 相关方法
InitialContext.lookup()

# 动态编译与执行
JavaCompiler

# Expression Language(EL 表达式)
ExpressionFactory

这里举例 2 种常见常见:

2. 反射机制导致的代码注入

Java反射允许通过类名、方法名动态调用代码,若这些参数可控且未过滤,会引发代码注入:

String ClassName = req.getParameter("ClassName"); // 用户可控类名
String MethodName = req.getParameter("Method"); // 用户可控方法名
String[] Args = new String[]{req.getParameter("Args").toString()}; // 用户可控参数

try {
    Class clazz = Class.forName(ClassName); // 加载用户指定的类
    Constructor constructor = clazz.getConstructor(String[].class);
    Object obj = constructor.newInstance(new Object[]{Args}); // 实例化对象
    Method method = clazz.getMethod(MethodName); // 获取用户指定的方法
    method.invoke(obj); // 执行方法
} catch (Exception e) {
    e.printStackTrace();
}

风险点:攻击者可通过ClassName传入危险类(如Runtime),通过MethodName调用exec等方法,间接执行系统命令或恶意代码。

3. 第三方组件漏洞示例(Apache Commons Collections)

这里引用《Java代码审计 (入门篇 )》的案例:Apache Commons Collections 3.1版本因反射机制的不当使用,在反序列化场景中存在代码注入漏洞。其核心是InvokerTransformer类的transform方法可动态调用任意方法:

public class InvokerTransformer implements Transformer {
    private String iMethodName; // 方法名(可控)
    private Class[] iParamTypes; // 参数类型(可控)
    private Object[] iArgs; // 参数(可控)

    public Object transform(Object input) {
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes); // 获取指定方法
            return method.invoke(input, this.iArgs); // 执行方法
        } catch (Exception e) {
            // 异常处理...
        }
    }
}

利用原理:攻击者通过反序列化控制iMethodNameiParamTypesiArgs,可调用URLClassLoader加载远程恶意类(如Evil),进而执行系统命令(如whoami),甚至构造回显获取执行结果。

三、C/C++ 中的注入场景及代码审计

C/C++ 作为系统级开发的核心语言,在嵌入式/终端系统底层接口开发(如防火墙规则配置、进程管理、设备控制等)中应用广泛。由于其直接与系统内核交互的特性,命令注入漏洞的危害更为直接,可能导致设备被完全控制。

1. C/C++ 中命令注入的危险函数及场景

C/C++ 中执行系统命令的函数主要依赖标准库或系统调用,以下是高风险函数及典型场景:

(1)system 函数(最常见风险点)

system(const char* command) 函数通过调用 shell 执行传入的命令字符串,若字符串包含用户可控输入且未过滤,会直接引发命令注入。

场景示例
系统防火墙配置接口可能允许用户配置 IP 黑名单,如下代码通过 system 执行 iptables 命令添加规则:

#include <stdlib.h>
#include <string.h>

// 假设user_ip来自用户输入(如应用配置、网络请求参数)
void add_firewall_blacklist(const char* user_ip) {
    char cmd[256];
    // 直接拼接用户输入到命令中,存在注入风险
    sprintf(cmd, "iptables -A INPUT -s %s -j DROP", user_ip);
    system(cmd); // 执行拼接后的命令
}

风险利用
若用户输入 192.168.1.1; rm -rf /,此时拼接后的命令为:
iptables -A INPUT -s 192.168.1.1; rm -rf / -j DROP
shell 会先执行 iptables 命令,再执行 rm -rf /,导致系统文件被删除。

(2)popen 函数(带输出的命令执行)

popen(const char* command, const char* mode) 用于执行命令并通过管道获取输出,其命令字符串的处理逻辑与 system 一致,同样存在注入风险。

场景示例
设备网络管理模块可能通过 popen 执行 ifconfig 查看用户指定网卡的信息:

#include <stdio.h>
#include <string.h>

void get_network_info(const char* interface) {
    char cmd[128];
    // 拼接用户输入的网卡名(如"eth0; whoami")
    sprintf(cmd, "ifconfig %s", interface);
    FILE* fp = popen(cmd, "r");
    // 读取并返回输出...
    pclose(fp);
}

风险利用
用户输入 eth0; cat /etc/passwd 时,命令会变为 ifconfig eth0; cat /etc/passwd,导致系统用户信息泄露。

(3)exec 系列函数(间接风险)

exec 系列函数(如 execlexecvexeclp 等)用于替换当前进程映像执行新命令。若通过字符串拼接构造命令参数,仍可能存在注入风险(尤其当使用 execlp 等依赖 shell 解析的函数时)。

风险示例

#include <unistd.h>
#include <string.h>

void execute_command(const char* user_input) {
    char cmd[128];
    // 拼接用户输入作为命令参数
    sprintf(cmd, "ping -c 4 %s", user_input);
    // execlp会通过shell解析命令字符串,等同于system
    execlp("/bin/sh", "sh", "-c", cmd, (char*)NULL);
}

风险点:与 system 类似,用户输入 8.8.8.8 && reboot 会导致设备重启。

2. 通用系统中的注入风险场景

在各类基于 C/C++ 开发的系统中,以下场景易出现命令注入风险:

  • 防火墙/网络规则配置:通过系统接口配置 iptablesnftables 等规则时,用户输入的 IP、端口、协议等参数若直接拼接进命令,可能被注入。
  • 设备管理命令:远程调试、进程启停、日志收集等功能(如系统命令封装接口)中,用户输入的设备 ID、进程名等未过滤,可能注入恶意命令。
  • 脚本引擎调用:通过 C/C++ 调用 shell 脚本(如 .sh 文件)时,若脚本参数包含用户输入且未处理,会间接导致注入。

3. C/C++ 代码审计要点(针对注入漏洞)

(1)危险函数定位

优先检索代码中是否包含以下函数,重点检查其参数来源:

  • systempopenpclose
  • exec 系列(尤其是 execlpexecvp 等依赖 shell 解析的函数)
  • 自定义命令执行封装函数(如各类系统命令调用接口)

(2)输入处理逻辑审计

  • 是否直接拼接用户可控输入:检查命令字符串是否通过 sprintfstrcat 等函数拼接用户可控变量(如网络输入、配置文件、IPC 消息等)。
  • 特殊字符过滤:是否过滤了命令分隔符(;&&&||)、命令替换符(`$(...))、重定向符(><)等。
  • 白名单验证:对用户输入(如 IP、端口、网卡名)是否采用白名单限制(如 IP 格式校验、端口范围限制)。

安全示例(对比修复)
对上述防火墙接口进行修复,通过白名单校验 IP 格式并过滤特殊字符以修复风险:

#include <stdlib.h>
#include <regex.h>
#include <string.h>

// 验证IP格式(简单示例)
int is_valid_ip(const char* ip) {
    regex_t regex;
    // 匹配IPv4地址的正则(简化版)
    const char* pattern = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
    if (regcomp(&regex, pattern, REG_EXTENDED) != 0) return 0;
    int ret = regexec(&regex, ip, 0, NULL, 0) == 0;
    regfree(&regex);
    return ret;
}

void add_firewall_blacklist_safe(const char* user_ip) {
    // 1. 验证IP合法性(白名单校验)
    if (!is_valid_ip(user_ip)) {
        // 日志记录+拒绝处理
        return;
    }
    // 2. 进一步过滤特殊字符(防御残余风险)
    for (int i = 0; user_ip[i] != '\0'; i++) {
        if (strchr(";|&`$<>", user_ip[i]) != NULL) {
            // 发现危险字符,拒绝处理
            return;
        }
    }
    // 3. 安全拼接命令
    char cmd[256];
    snprintf(cmd, sizeof(cmd), "iptables -A INPUT -s %s -j DROP", user_ip);
    system(cmd);
}

4. 审计工具与辅助手段

  • 静态分析:使用 Clang 静态分析器(scan-build)或系统开发工具链中的漏洞扫描模块,检测 system 等函数的参数是否包含用户输入。
  • 动态测试:在测试环境中向可疑接口传入含特殊字符的输入(如 ;id&&whoami),观察是否执行恶意命令。
  • 依赖检查:检查系统中是否使用了第三方 C/C++ 库(如命令解析库),确认其是否存在命令注入相关历史漏洞。

总结

命令注入和代码注入均为高危注入类漏洞,其核心成因在于“用户可控输入未严格过滤就被当作命令/代码执行”。安全测试人员在检测时需重点关注:

  1. 危险函数:Python中的execos.system,Java中的Runtime.exec()Class.forName()Method.invoke()等,检查其参数是否包含用户输入。
  2. 输入处理逻辑:验证程序是否对用户输入进行了严格过滤(如禁止特殊字符|&,限制类名/方法名白名单)。
  3. 组件风险:关注第三方组件(如Apache Commons Collections)的历史漏洞,结合反序列化等场景检测潜在注入点。

命令注入在 C/C++ 系统级开发中危害极大,核心风险源于“用户输入未严格验证即作为命令执行”。审计时需重点关注 systempopen 等危险函数的参数处理逻辑,结合通用系统接口(如防火墙、设备管理),通过白名单校验、特殊字符过滤、参数化命令调用等方式消除风险,确保用户输入无法突破预期执行范围。

本文是「Web安全」系列内容,点击专栏导航查看全部内容。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐