mirror of
https://github.com/hashcat/hashcat.git
synced 2024-11-21 23:58:07 +00:00
make our hash extraction scripts executable
This commit is contained in:
parent
f161c3c181
commit
a6c21e7ffc
1
tools/bitwarden2hashcat.py
Normal file → Executable file
1
tools/bitwarden2hashcat.py
Normal file → Executable file
@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
"""Utility to extract Bitwarden hash for hashcat from Google Chrome / Firefox / Desktop local data"""
|
"""Utility to extract Bitwarden hash for hashcat from Google Chrome / Firefox / Desktop local data"""
|
||||||
|
|
||||||
#
|
#
|
||||||
|
0
tools/metamask2hashcat.py
Normal file → Executable file
0
tools/metamask2hashcat.py
Normal file → Executable file
547
tools/mozilla2hashcat.py
Normal file → Executable file
547
tools/mozilla2hashcat.py
Normal file → Executable file
@ -1,273 +1,274 @@
|
|||||||
#
|
#!/usr/bin/env python
|
||||||
# Script to extract the "hash" from a password protected key3.db or key4.db file.
|
#
|
||||||
#
|
# Script to extract the "hash" from a password protected key3.db or key4.db file.
|
||||||
# This code is based on the tool "firepwd" (https://github.com/lclevy/firepwd (GPL-license)
|
#
|
||||||
# Although the code has been changed a bit, all credit goes to @lclevy for his initial work.
|
# This code is based on the tool "firepwd" (https://github.com/lclevy/firepwd (GPL-license)
|
||||||
#
|
# Although the code has been changed a bit, all credit goes to @lclevy for his initial work.
|
||||||
# Tested with Python 3.8.5 and the following libraries: PyCryptodome 3.10.1 and pyasn1 0.4.8
|
#
|
||||||
#
|
# Tested with Python 3.8.5 and the following libraries: PyCryptodome 3.10.1 and pyasn1 0.4.8
|
||||||
# Author:
|
#
|
||||||
# https://github.com/Banaanhangwagen
|
# Author:
|
||||||
# https://github.com/mneitsabes
|
# https://github.com/Banaanhangwagen
|
||||||
# License: MIT
|
# https://github.com/mneitsabes
|
||||||
#
|
# License: MIT
|
||||||
|
#
|
||||||
import argparse
|
|
||||||
from collections import namedtuple
|
import argparse
|
||||||
import enum
|
from collections import namedtuple
|
||||||
import binascii
|
import enum
|
||||||
import hashlib
|
import binascii
|
||||||
import hmac
|
import hashlib
|
||||||
import os
|
import hmac
|
||||||
import sqlite3
|
import os
|
||||||
import struct
|
import sqlite3
|
||||||
import sys
|
import struct
|
||||||
|
import sys
|
||||||
from Crypto.Cipher import AES, DES3
|
|
||||||
from pyasn1.codec.der import decoder
|
from Crypto.Cipher import AES, DES3
|
||||||
|
from pyasn1.codec.der import decoder
|
||||||
|
|
||||||
class MasterPasswordInfos:
|
|
||||||
def __init__(self, mode, global_salt, entry_salt, cipher_text, no_master_password, iteration=None, iv=None):
|
class MasterPasswordInfos:
|
||||||
if mode not in ['aes', '3des']:
|
def __init__(self, mode, global_salt, entry_salt, cipher_text, no_master_password, iteration=None, iv=None):
|
||||||
raise ValueError('Bad mode')
|
if mode not in ['aes', '3des']:
|
||||||
|
raise ValueError('Bad mode')
|
||||||
self.mode = mode
|
|
||||||
self.global_salt = global_salt
|
self.mode = mode
|
||||||
self.entry_salt = entry_salt
|
self.global_salt = global_salt
|
||||||
self.cipher_text = cipher_text
|
self.entry_salt = entry_salt
|
||||||
self.no_master_password = no_master_password
|
self.cipher_text = cipher_text
|
||||||
self.iteration = iteration
|
self.no_master_password = no_master_password
|
||||||
self.iv = iv
|
self.iteration = iteration
|
||||||
|
self.iv = iv
|
||||||
|
|
||||||
def read_bsd_db(db_filepath: str) -> {}:
|
|
||||||
"""
|
def read_bsd_db(db_filepath: str) -> {}:
|
||||||
Read the key3.db.
|
"""
|
||||||
|
Read the key3.db.
|
||||||
:param db_filepath: the database filepath
|
|
||||||
:type db_filepath: str
|
:param db_filepath: the database filepath
|
||||||
:return: the dict
|
:type db_filepath: str
|
||||||
:rtype: dict
|
:return: the dict
|
||||||
"""
|
:rtype: dict
|
||||||
with open(db_filepath, 'rb') as f:
|
"""
|
||||||
header = f.read(4 * 15)
|
with open(db_filepath, 'rb') as f:
|
||||||
|
header = f.read(4 * 15)
|
||||||
magic = struct.unpack('>L', header[0:4])[0]
|
|
||||||
if magic != 0x61561:
|
magic = struct.unpack('>L', header[0:4])[0]
|
||||||
raise ValueError('Bad magic number')
|
if magic != 0x61561:
|
||||||
|
raise ValueError('Bad magic number')
|
||||||
version = struct.unpack('>L', header[4:8])[0]
|
|
||||||
if version != 2:
|
version = struct.unpack('>L', header[4:8])[0]
|
||||||
raise ValueError('Bad version')
|
if version != 2:
|
||||||
|
raise ValueError('Bad version')
|
||||||
pagesize = struct.unpack('>L', header[12:16])[0]
|
|
||||||
nkeys = struct.unpack('>L', header[56:60])[0]
|
pagesize = struct.unpack('>L', header[12:16])[0]
|
||||||
|
nkeys = struct.unpack('>L', header[56:60])[0]
|
||||||
readkeys = 0
|
|
||||||
page = 1
|
readkeys = 0
|
||||||
db1 = []
|
page = 1
|
||||||
|
db1 = []
|
||||||
while readkeys < nkeys:
|
|
||||||
f.seek(pagesize*page)
|
while readkeys < nkeys:
|
||||||
|
f.seek(pagesize*page)
|
||||||
offsets = f.read((nkeys+1) * 4 + 2)
|
|
||||||
offset_vals = []
|
offsets = f.read((nkeys+1) * 4 + 2)
|
||||||
i = 0
|
offset_vals = []
|
||||||
nval = 0
|
i = 0
|
||||||
val = 1
|
nval = 0
|
||||||
keys = 0
|
val = 1
|
||||||
|
keys = 0
|
||||||
while nval != val:
|
|
||||||
keys += 1
|
while nval != val:
|
||||||
key = struct.unpack('<H', offsets[(2+i):(2+i)+2])[0]
|
keys += 1
|
||||||
val = struct.unpack('<H', offsets[(4+i):(4+i)+2])[0]
|
key = struct.unpack('<H', offsets[(2+i):(2+i)+2])[0]
|
||||||
nval = struct.unpack('<H', offsets[(8+i):(8+i)+2])[0]
|
val = struct.unpack('<H', offsets[(4+i):(4+i)+2])[0]
|
||||||
|
nval = struct.unpack('<H', offsets[(8+i):(8+i)+2])[0]
|
||||||
offset_vals.append(key + (pagesize * page))
|
|
||||||
offset_vals.append(val + (pagesize * page))
|
offset_vals.append(key + (pagesize * page))
|
||||||
readkeys += 1
|
offset_vals.append(val + (pagesize * page))
|
||||||
i += 4
|
readkeys += 1
|
||||||
|
i += 4
|
||||||
offset_vals.append(pagesize * (page + 1))
|
|
||||||
val_key = sorted(offset_vals)
|
offset_vals.append(pagesize * (page + 1))
|
||||||
|
val_key = sorted(offset_vals)
|
||||||
for i in range(keys * 2):
|
|
||||||
f.seek(val_key[i])
|
for i in range(keys * 2):
|
||||||
data = f.read(val_key[i+1] - val_key[i])
|
f.seek(val_key[i])
|
||||||
db1.append(data)
|
data = f.read(val_key[i+1] - val_key[i])
|
||||||
|
db1.append(data)
|
||||||
page += 1
|
|
||||||
|
page += 1
|
||||||
db = {}
|
|
||||||
for i in range(0, len(db1), 2):
|
db = {}
|
||||||
db[db1[i+1]] = db1[i]
|
for i in range(0, len(db1), 2):
|
||||||
|
db[db1[i+1]] = db1[i]
|
||||||
return db
|
|
||||||
|
return db
|
||||||
|
|
||||||
def is_decrypting_mozilla_3des_without_master_password(global_salt, entry_salt, cipher_text):
|
|
||||||
"""
|
def is_decrypting_mozilla_3des_without_master_password(global_salt, entry_salt, cipher_text):
|
||||||
Indicate if the cipher_text can be decrypted to 'password-check\x02\x02' without a master password
|
"""
|
||||||
in the the "mozilla 3DES"
|
Indicate if the cipher_text can be decrypted to 'password-check\x02\x02' without a master password
|
||||||
|
in the the "mozilla 3DES"
|
||||||
:param global_salt: the global salt
|
|
||||||
:param entry_salt: the entry salt
|
:param global_salt: the global salt
|
||||||
:param cipher_text: the encrypted text
|
:param entry_salt: the entry salt
|
||||||
:return: the decrypted text
|
:param cipher_text: the encrypted text
|
||||||
"""
|
:return: the decrypted text
|
||||||
hp = hashlib.sha1(global_salt).digest()
|
"""
|
||||||
pes = entry_salt + b'\x00'*(20-len(entry_salt))
|
hp = hashlib.sha1(global_salt).digest()
|
||||||
chp = hashlib.sha1(hp + entry_salt).digest()
|
pes = entry_salt + b'\x00'*(20-len(entry_salt))
|
||||||
k1 = hmac.new(chp, pes + entry_salt, hashlib.sha1).digest()
|
chp = hashlib.sha1(hp + entry_salt).digest()
|
||||||
tk = hmac.new(chp, pes, hashlib.sha1).digest()
|
k1 = hmac.new(chp, pes + entry_salt, hashlib.sha1).digest()
|
||||||
k2 = hmac.new(chp, tk + entry_salt, hashlib.sha1).digest()
|
tk = hmac.new(chp, pes, hashlib.sha1).digest()
|
||||||
k = k1 + k2
|
k2 = hmac.new(chp, tk + entry_salt, hashlib.sha1).digest()
|
||||||
iv = k[-8:]
|
k = k1 + k2
|
||||||
key = k[:24]
|
iv = k[-8:]
|
||||||
|
key = k[:24]
|
||||||
return DES3.new(key, DES3.MODE_CBC, iv).decrypt(cipher_text) == b'password-check\x02\x02'
|
|
||||||
|
return DES3.new(key, DES3.MODE_CBC, iv).decrypt(cipher_text) == b'password-check\x02\x02'
|
||||||
|
|
||||||
def is_decrypting_pbe_aes_without_password(global_salt, entry_salt, iteration, iv, cipher_text):
|
|
||||||
"""
|
def is_decrypting_pbe_aes_without_password(global_salt, entry_salt, iteration, iv, cipher_text):
|
||||||
Indicate if the cipher_text can be decrypted to password-check\x02\x02' without a master password
|
"""
|
||||||
in the the AES mode.
|
Indicate if the cipher_text can be decrypted to password-check\x02\x02' without a master password
|
||||||
|
in the the AES mode.
|
||||||
:param global_salt: the global salt
|
|
||||||
:param entry_salt: the entry salt
|
:param global_salt: the global salt
|
||||||
:param iteration: the number of iteration
|
:param entry_salt: the entry salt
|
||||||
:param iv: the iv
|
:param iteration: the number of iteration
|
||||||
:param cipher_text: the encrypted text
|
:param iv: the iv
|
||||||
:return: the decrypted text
|
:param cipher_text: the encrypted text
|
||||||
"""
|
:return: the decrypted text
|
||||||
k = hashlib.sha1(global_salt).digest()
|
"""
|
||||||
key = hashlib.pbkdf2_hmac('sha256', k, entry_salt, iteration, dklen=32)
|
k = hashlib.sha1(global_salt).digest()
|
||||||
|
key = hashlib.pbkdf2_hmac('sha256', k, entry_salt, iteration, dklen=32)
|
||||||
return AES.new(key, AES.MODE_CBC, iv).decrypt(cipher_text) == b'password-check\x02\x02'
|
|
||||||
|
return AES.new(key, AES.MODE_CBC, iv).decrypt(cipher_text) == b'password-check\x02\x02'
|
||||||
|
|
||||||
def extract_master_password_infos(db_filepath: str, db_version: int) -> MasterPasswordInfos:
|
|
||||||
"""
|
def extract_master_password_infos(db_filepath: str, db_version: int) -> MasterPasswordInfos:
|
||||||
Extract the master password information from the database.
|
"""
|
||||||
|
Extract the master password information from the database.
|
||||||
:param db_filepath: the db filepath
|
|
||||||
:type db_filepath: str
|
:param db_filepath: the db filepath
|
||||||
:param db_version: the db_type, 3 or 4
|
:type db_filepath: str
|
||||||
:type db_version: int
|
:param db_version: the db_type, 3 or 4
|
||||||
:return: the infos
|
:type db_version: int
|
||||||
:rtype: MasterPasswordInfos
|
:return: the infos
|
||||||
"""
|
:rtype: MasterPasswordInfos
|
||||||
if db_version not in [3, 4]:
|
"""
|
||||||
raise ValueError('db_version not supported')
|
if db_version not in [3, 4]:
|
||||||
|
raise ValueError('db_version not supported')
|
||||||
if db_version == 3:
|
|
||||||
db_values = read_bsd_db(db_filepath)
|
if db_version == 3:
|
||||||
|
db_values = read_bsd_db(db_filepath)
|
||||||
global_salt = db_values[b'global-salt']
|
|
||||||
pwd_check = db_values[b'password-check']
|
global_salt = db_values[b'global-salt']
|
||||||
entry_salt_len = pwd_check[1]
|
pwd_check = db_values[b'password-check']
|
||||||
entry_salt = pwd_check[3: 3 + entry_salt_len]
|
entry_salt_len = pwd_check[1]
|
||||||
cipher_text = pwd_check[-16:]
|
entry_salt = pwd_check[3: 3 + entry_salt_len]
|
||||||
|
cipher_text = pwd_check[-16:]
|
||||||
no_master_password = is_decrypting_mozilla_3des_without_master_password(global_salt, entry_salt, cipher_text)
|
|
||||||
|
no_master_password = is_decrypting_mozilla_3des_without_master_password(global_salt, entry_salt, cipher_text)
|
||||||
return MasterPasswordInfos('3des', global_salt, entry_salt, cipher_text, no_master_password)
|
|
||||||
else:
|
return MasterPasswordInfos('3des', global_salt, entry_salt, cipher_text, no_master_password)
|
||||||
db = sqlite3.connect(db_filepath)
|
else:
|
||||||
c = db.cursor()
|
db = sqlite3.connect(db_filepath)
|
||||||
c.execute('SELECT item1,item2 FROM metadata WHERE id = "password"')
|
c = db.cursor()
|
||||||
global_salt, encoded_item2 = c.fetchone()
|
c.execute('SELECT item1,item2 FROM metadata WHERE id = "password"')
|
||||||
decoded_item2 = decoder.decode(encoded_item2)
|
global_salt, encoded_item2 = c.fetchone()
|
||||||
|
decoded_item2 = decoder.decode(encoded_item2)
|
||||||
pbe_algo = str(decoded_item2[0][0][0])
|
|
||||||
if pbe_algo == '1.2.840.113549.1.12.5.1.3': # pbeWithSha1AndTripleDES-CBC
|
pbe_algo = str(decoded_item2[0][0][0])
|
||||||
entry_salt = decoded_item2[0][0][1][0].asOctets()
|
if pbe_algo == '1.2.840.113549.1.12.5.1.3': # pbeWithSha1AndTripleDES-CBC
|
||||||
cipher_text = decoded_item2[0][1].asOctets()
|
entry_salt = decoded_item2[0][0][1][0].asOctets()
|
||||||
|
cipher_text = decoded_item2[0][1].asOctets()
|
||||||
no_master_password = is_decrypting_mozilla_3des_without_master_password(global_salt, entry_salt,
|
|
||||||
cipher_text)
|
no_master_password = is_decrypting_mozilla_3des_without_master_password(global_salt, entry_salt,
|
||||||
return MasterPasswordInfos('3des', global_salt, entry_salt, cipher_text, no_master_password)
|
cipher_text)
|
||||||
elif pbe_algo == '1.2.840.113549.1.5.13': # pkcs5 pbes2
|
return MasterPasswordInfos('3des', global_salt, entry_salt, cipher_text, no_master_password)
|
||||||
assert str(decoded_item2[0][0][1][0][0]) == '1.2.840.113549.1.5.12'
|
elif pbe_algo == '1.2.840.113549.1.5.13': # pkcs5 pbes2
|
||||||
assert str(decoded_item2[0][0][1][0][1][3][0]) == '1.2.840.113549.2.9'
|
assert str(decoded_item2[0][0][1][0][0]) == '1.2.840.113549.1.5.12'
|
||||||
assert str(decoded_item2[0][0][1][1][0]) == '2.16.840.1.101.3.4.1.42'
|
assert str(decoded_item2[0][0][1][0][1][3][0]) == '1.2.840.113549.2.9'
|
||||||
assert int(decoded_item2[0][0][1][0][1][2]) == 32 # key length
|
assert str(decoded_item2[0][0][1][1][0]) == '2.16.840.1.101.3.4.1.42'
|
||||||
|
assert int(decoded_item2[0][0][1][0][1][2]) == 32 # key length
|
||||||
entry_salt = decoded_item2[0][0][1][0][1][0].asOctets()
|
|
||||||
iteration = int(decoded_item2[0][0][1][0][1][1])
|
entry_salt = decoded_item2[0][0][1][0][1][0].asOctets()
|
||||||
iv = b'\x04\x0e' + decoded_item2[0][0][1][1][1].asOctets()
|
iteration = int(decoded_item2[0][0][1][0][1][1])
|
||||||
cipher_text = decoded_item2[0][1].asOctets()
|
iv = b'\x04\x0e' + decoded_item2[0][0][1][1][1].asOctets()
|
||||||
|
cipher_text = decoded_item2[0][1].asOctets()
|
||||||
no_master_password = is_decrypting_pbe_aes_without_password(global_salt, entry_salt, iteration, iv,
|
|
||||||
cipher_text)
|
no_master_password = is_decrypting_pbe_aes_without_password(global_salt, entry_salt, iteration, iv,
|
||||||
return MasterPasswordInfos('aes', global_salt, entry_salt, cipher_text, no_master_password, iteration, iv)
|
cipher_text)
|
||||||
|
return MasterPasswordInfos('aes', global_salt, entry_salt, cipher_text, no_master_password, iteration, iv)
|
||||||
def hex(b) -> str:
|
|
||||||
"""
|
def hex(b) -> str:
|
||||||
Returns the hexily version of the binary datas.
|
"""
|
||||||
|
Returns the hexily version of the binary datas.
|
||||||
:param b: binary datas
|
|
||||||
:return: the string
|
:param b: binary datas
|
||||||
"""
|
:return: the string
|
||||||
return binascii.hexlify(b).decode('utf8')
|
"""
|
||||||
|
return binascii.hexlify(b).decode('utf8')
|
||||||
|
|
||||||
def get_hashcat_string(mpinfos: MasterPasswordInfos) -> str:
|
|
||||||
"""
|
def get_hashcat_string(mpinfos: MasterPasswordInfos) -> str:
|
||||||
Print the hashchat format string.
|
"""
|
||||||
|
Print the hashchat format string.
|
||||||
:param mpinfos: the infos
|
|
||||||
:type mpinfos: MasterPasswordInfos
|
:param mpinfos: the infos
|
||||||
:return: the string
|
:type mpinfos: MasterPasswordInfos
|
||||||
:rtype: str
|
:return: the string
|
||||||
"""
|
:rtype: str
|
||||||
|
"""
|
||||||
if mpinfos.no_master_password:
|
|
||||||
return 'No Primary Password is set.'
|
if mpinfos.no_master_password:
|
||||||
else:
|
return 'No Primary Password is set.'
|
||||||
s = '$mozilla$*'
|
else:
|
||||||
|
s = '$mozilla$*'
|
||||||
if mpinfos.mode == '3des':
|
|
||||||
s += f'3DES*{hex(mpinfos.global_salt)}*{hex(mpinfos.entry_salt)}*{hex(mpinfos.cipher_text)}'
|
if mpinfos.mode == '3des':
|
||||||
else:
|
s += f'3DES*{hex(mpinfos.global_salt)}*{hex(mpinfos.entry_salt)}*{hex(mpinfos.cipher_text)}'
|
||||||
s += f'AES*{hex(mpinfos.global_salt)}*{hex(mpinfos.entry_salt)}*{mpinfos.iteration}*' \
|
else:
|
||||||
f'{hex(mpinfos.iv)}*{hex(mpinfos.cipher_text)}'
|
s += f'AES*{hex(mpinfos.global_salt)}*{hex(mpinfos.entry_salt)}*{mpinfos.iteration}*' \
|
||||||
|
f'{hex(mpinfos.iv)}*{hex(mpinfos.cipher_text)}'
|
||||||
return s
|
|
||||||
|
return s
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
usage = 'python3 mozilla2hashcat.py <profile_directory or key3/4.db file>\n'
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, usage=usage)
|
usage = 'python3 mozilla2hashcat.py <profile_directory or key3/4.db file>\n'
|
||||||
parser.add_argument('dir_or_db', help='The directory containing the key3/4.db-file or the key3/4.db-file itself')
|
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, usage=usage)
|
||||||
args = parser.parse_args()
|
parser.add_argument('dir_or_db', help='The directory containing the key3/4.db-file or the key3/4.db-file itself')
|
||||||
|
args = parser.parse_args()
|
||||||
db_filepath = None
|
|
||||||
db_type = None
|
db_filepath = None
|
||||||
|
db_type = None
|
||||||
if os.path.isdir(args.dir_or_db):
|
|
||||||
db3_filepath = os.path.join(args.dir_or_db, 'key3.db')
|
if os.path.isdir(args.dir_or_db):
|
||||||
db4_filepath = os.path.join(args.dir_or_db, 'key4.db')
|
db3_filepath = os.path.join(args.dir_or_db, 'key3.db')
|
||||||
|
db4_filepath = os.path.join(args.dir_or_db, 'key4.db')
|
||||||
if os.path.exists(db3_filepath):
|
|
||||||
db_filepath = db3_filepath
|
if os.path.exists(db3_filepath):
|
||||||
db_type = 3
|
db_filepath = db3_filepath
|
||||||
elif os.path.exists(db4_filepath):
|
db_type = 3
|
||||||
db_filepath = db4_filepath
|
elif os.path.exists(db4_filepath):
|
||||||
db_type = 4
|
db_filepath = db4_filepath
|
||||||
elif os.path.isfile(args.dir_or_db):
|
db_type = 4
|
||||||
filename = os.path.basename(args.dir_or_db)
|
elif os.path.isfile(args.dir_or_db):
|
||||||
if filename == 'key3.db':
|
filename = os.path.basename(args.dir_or_db)
|
||||||
db_filepath = args.dir_or_db
|
if filename == 'key3.db':
|
||||||
db_type = 3
|
db_filepath = args.dir_or_db
|
||||||
elif filename == 'key4.db':
|
db_type = 3
|
||||||
db_filepath = args.dir_or_db
|
elif filename == 'key4.db':
|
||||||
db_type = 4
|
db_filepath = args.dir_or_db
|
||||||
|
db_type = 4
|
||||||
if not db_filepath:
|
|
||||||
sys.stderr.write('key3.db or key4.db file not found\n')
|
if not db_filepath:
|
||||||
exit(-1)
|
sys.stderr.write('key3.db or key4.db file not found\n')
|
||||||
|
exit(-1)
|
||||||
infos = extract_master_password_infos(db_filepath, db_type)
|
|
||||||
print(get_hashcat_string(infos))
|
infos = extract_master_password_infos(db_filepath, db_type)
|
||||||
|
print(get_hashcat_string(infos))
|
||||||
|
0
tools/virtualbox2hashcat.py
Normal file → Executable file
0
tools/virtualbox2hashcat.py
Normal file → Executable file
0
tools/vmwarevmx2hashcat.py
Normal file → Executable file
0
tools/vmwarevmx2hashcat.py
Normal file → Executable file
Loading…
Reference in New Issue
Block a user