                  . o O o . I N S I D E   N D S . o O o .
                  . o O o . . . . . . . . . . . . o O o .

The following paper -- Inside NDS -- explains the Pandora research project 
and what has been discovered so far. It was originally released on June 27, 
1997 and was revised on March 10, 1998. Almost a year ago L0pht Heavy 
Industries <www.l0pht.com> released the first version of L0phtCrack, a program
designed to take advantage of Windows NT's dependency on legacy systems and
crack passwords. After Novell tried to rake Microsoft over the coals by 
stating that not even administrators could access password hashes, let alone
recover the actual passwords from Netware, Pandora was born.


                     . o O o . [ Abstract ] . o O o .

This document will present a technical view of the layout of Novell's Netware 
Directory Services (NDS). The emphasis here is mainly from a security 
perspective, and tries to point out several areas of weakness that need to be
reinforced. Novell has touted the advanced security features built into NDS 
as being superior to other network operating systems, including statements 
that passwords cannot be recovered, even by administrators. This document
will examine how to recover the passwords, and give a complete layout of NDS.

                     . o O o . [ Audience ] . o O o .

While I am probably more well known for the Netware Hack FAQ and presentation 
of security issues related to Netware from an intruder perspective, this 
document is geared more toward those simply interested in how things work. 
But due to the fact that I am covering security issues, the inference is still
there.

I do assume some basic Netware knowledge, and I make use of some examples of C
code to explain some concepts, therefore knowledge of C coding might be of 
some help. I have tried to make this as non-technical as possible, but due to
the material being covered, unless you are a bit head this is probably going
to be a very dry read.


                     . o O o . [ Credits ] . o O o .

If you read nothing, or just a paragraph or two, please at least read these
credits. I did have some support during this project, and I want to acknowledge
a few people. For some background help and some code snippets, itsme 
<itsme@xs4all.nl> provided a lot. On version 1.0 I bounced ideas off of Greg
Miller <greg.miller@usa.net>, Al Grant <ag129@cam.ac.uk>, and Rx2 <rx2@usa.net>.
Thanks a lot. And with version 2.0 I received a number of comments regarding 
the code, including example source code. While I incorporated a number of 
features suggested by Thomas Lackner <alackn01@fiu.edu>, it was the changes 
submitted by Jitsu-Disk <golgo13@pratique.fr> that drastically changed things. 
The speed increase is absolutely astounding. On a Pentium 100 Mhz the speed 
increase was 76 times faster. That's not a typo, 76 times faster, not 76% 
faster. His paper explaining how he rewrote the itsme-supplied proof-of-concept
code is included in Pandora 2.0.

Lab assistance was provided by Mr. Wizard <otter@fastlane.net> and Fourth
Stooge <stooge@onramp.net>. Mr. Wizard provided several examples of BACKUP.DS 
so I had multiple copies to play with, and Fourth Stooge provided hardware in 
the form of hard drives for the lab. Without the drive space I would have been 
very limited, and without the extra BACKUP.DS files I would not have been able 
to accurately cross check the different 4.x Netware versions for consistancy.

Finally I would like to thank two more people, my wife <loving.bitch@nmrc.org>
for naming the project Pandora after hearing my explanation of what the hell I 
was doing in the lab for hours at a time, and Marcus Williamson 
<71333.1665@compuserve.com> for trying to keep me honest through a series of 
email exchanges where he constantly tried to get me to admit that this was NOT 
a security breach (that is left for you, the humble reader, to decide).

                       . o O o . [ Tools ] . o O o .

There were a number of tools used in the preparation of this document. The two
main tools were a hex dump utility and a hex calculator. By examining the NDS 
files in the hex dump utility and playing with some of the values, a picture 
of how the files were tied together emerged. I also used a C compiler and 
wrote several utilities to extract and examine the data. Most of the utilities
were short programs of little use to the general public, but as I continued to
explore I wrote more useful utilities for NDS extraction. These utilities, 
along with a copy of this document were released as a set called Pandora. 
Pandora is available from http://www.nmrc.org/files/netware/pandora.zip, or 
from a link on the Pandora Home Page at http://www.nmrc.org/pandora.

                    . o O o . [ Background ] . o O o .

NDS is a distributed database for Netware 4.x that provides access to all 
network resources. It allows a user to use a single login to a Netware 
environment and approach a group of Netware servers as a single entity. GUI 
interfaces provide easy management for administrators.

NDS itself consists of 4 core files. These files include PARTITIO.NDS, 
ENTRY.NDS, VALUE.NDS, and BLOCK.NDS. The files are stored on the SYS: volume 
in a hidden directory called _NETWARE. This directory cannot be directly 
accessed from a user login session, including an administrator.

All objects addressed by the server are located within the ENTRY.NDS file. All 
named attributes have a record, and all administrator-created items have a 
record. For example, there is a record called USER which contains information 
about the USER property itself, and a record for a user called Admin which 
contains information about that particular USER object.

Values associated with ENTRY records are stored in one and sometimes two files.
VALUE.NDS will contain up to 16 bytes of data about an ENTRY record. Why so 
little? Well, 16 bytes is EXACTLY what is needed for one ACL entry. ACL entries
are the most common VALUE records. If more than 16 bytes of information is 
needed, the VALUE record has a pointer to BLOCK.NDS. This file's records can 
contain up to 108 bytes of data. If still more room is needed, extra BLOCK 
records can be linked together via pointers.

The partition information is contained within PARTITIO.NDS, which is basically 
used to keep track of a minimal amount of information that helps NDS replicate 
and sync up the data between servers.

                    . o O o . [ Accessing NDS ] . o O o .

First off, to explore NDS one must retrieve a copy from a server. This is 
actually easier than it seems. The two main ways to get copies of NDS involve 
console access, and will interrupt server access during retrieval. If you are 
pulling a copy of NDS off of a server, make sure you wait until a time when 
user activity is at a minimal. The interruption will mean that access to 
network resources that have not been authenticated will not work. If users are 
already logged in, they should not notice any interruptions. This is similiar 
to the impact encountered during a DSREPAIR.

The first method involves using either RCONSOLE or direct console access and 
loading an NLM that allows access to SYS:_NETWARE to retrieve the copies. Two 
such NLMs are JCMD.NLM and NETBASIC.NLM. JCMD.NLM is freeware available on the 
Internet, but NETBASIC.NLM is the prefered method, as this NLM is bundled with 
Netware 4.11. Loading NETBASIC and typing "shell" drops you to a pseudo 
DOS-like environment. From here you can simply cd into SYS:_NETWARE, and copy 
the *.NDS files to another location on the server (for example SYS:LOGIN). 
Remember, DS.NLM must be unloaded to do this on larger systems. I have
received reports of small LANs allowing copies to be made while DS.NLM was 
loaded. YMMV.

For the faint of heart, there is one other method using DSMAINT.NLM, or 
NDSCOPY.NLM that came with an older version of DSMAINT.NLM. These are 
available from Novell's web site. The by-product of running these NLMs is a 
file called BACKUP.DS that is placed in the SYS:SYSTEM directory. This is my 
prefered method of retrieval, and you get all of the NDS files in one package.

It should be obvious, but if your server console is compromised, an intruder 
could do this as well. And if the file system is compromised, BACKUP.DS will be
a tempting target. So keep BACKUP.DS in a safe place if you wish to protect NDS,
and if no longer needed, delete it.

                  . o O o . [ NDS File Structure ] . o O o .

The 4 files -- ENTRY.NDS, VALUE.NDS, BLOCK.NDS, and PARTITIO.NDS -- are binary 
files that consist of individual records. Here is the structure (as well as I 
can determine without the source code) for each record. Included are my 
comments:

typedef unsigned long uint32;
typedef unsigned int  uint16;
typedef unsigned char uint8;

/*
 * struct for ENTRY.NDS records
 */
typedef struct entry
{
	uint32          selfOffset;    /* Offset in ENTRY.NDS. If this is
					  the first record, it is 0x00000000
					  followed by 0x0000014e for the
					  second record, etc. */
	uint32          checkSum;      /* I assume a checksum */
	uint32          val1;          /* Unsure, 0xfeffffff. */
	uint32          val2;          /* Unsure, 0xffffffff. */
	uint32          peer;          /* Offset to a peer record. */
	uint32          firstChild;    /* Offset to first child record. If
					  no kids, 0xffffffff. */
	uint32          lastChild;     /* Offset to second child record. If
					  no kids, 0xffffffff. */
	uint32          firstValue;    /* Offset in VALUE.NDS of first
					  attribute. They are usually kept
					  in order in VALUE.NDS, but since
					  they are crossed referenced in
					  VALUE.NDS they don't have to be.*/
	uint32          id;            /* The Object ID of the record. */

	uint32          partitionID;   /* The partition ID of the record. */
	uint32          parentID;      /* The parent's Object ID, if no
					  parent it is 0xffffffff. */
	uint32          val3;          /* No idea. Usually a small number.*/
	uint32          val4;          /* No idea. 0x00000000. */
	uint32          subordinates;  /* Number of subordinates. This can
					  include other objects besides
					  children. */
	uint32          classID;       /* The "type" of Object ID. */
	uint32		creatTime1,    /* When object was created. */
			creatTime2;
	uint32		modTime1,      /* When object was last modified. */
			modTime2;
	uint8           name[258];     /* Dreaded unicode describing
					  the record. If a user object
					  it will be the common name. */
} ENTRY; /* size=334 */

/*
 * struct for VALUE.NDS records
 */
typedef struct value
{
	uint32          selfOffset;    /* Offset in VALUE.NDS. If this is
					  the first record, it is 0x00000000
					  followed by 0x00000040 for the
					  second record, etc. */
	uint32          checkSum;      /* I assume a checksum */
	uint32          val1;          /* Unsure, usually 0xfeffffff. */
	uint32          val2;          /* Unsure, usually 0xffffffff. */
	uint32          nextVal;       /* The next Value record's offset. */
	uint32          firstBlock;    /* Offset in BLOCK.NDS if used. */
	uint32          entryID;       /* Type of record in ENTRY.NDS. */
	uint32          typeID;        /* Type of VALUE record. */
	uint32          val3;          /* No idea. Usually a small number.*/
	uint32          creatTime1,    /* When object was created(?), */
			creatTime2;    /*  and modified(?). */
	uint32          length;        /* Length of data. */
	uint8           data[16];      /* Start of data, unless there is a
					  small amount of data, then it's
					  all here. */
} VALUE; /* size=64  */

/*
 * struct for BLOCK.NDS records
 */
typedef struct block
{
	uint32          selfOffset;    /* Offset in BLOCK.NDS. If this is
					  the first record, it is 0x00000000
					  followed by 0x00000080 for the
					  second record, etc. */
	uint32          checkSum;      /* I assume a checksum */
	uint32          val1;          /* Unsure. */
	uint32          nextBlock;     /* Next record if data>108. */
	uint32          valueOffset;   /* Offset in VALUE.NDS (backlink) */
	uint8           data[108];
} BLOCK; /* size=128 */

/*
 * struct for PARTITIO.NDS records
 */
typedef struct partition
{
	uint32          selfOffset;    /* Offset in PARTITIO.NDS. If this is
					  the first record, it is 0x00000000
					  followed by 0x00000028 for the
					  second record, etc. */
	uint32          checkSum;      /* I assume a checksum */
	uint32          val1;          /* Unsure. */
	uint32          id;            /* ID of record. */
	uint32          entryID;       /* ID in ENTRY.NDS */
	uint32          replicaID;     /* Replica ID (??) in ENTRY.NDS */
	uint32          val2;          /* Unsure. */
	uint32          val3;          /* Unsure. */
	uint32          timeStamp1,    /* Probably used to keep things in sync */
			timeStamp2;
} PARTITIO; /* size=40 */

As you can see I've had to guess at a lot of these, but I think there is enough
there to allow you to see what is in NDS. By comparing the BACKUP.DS and *.NDS 
files, along with the selfOffset at the beginning of each record, you can not 
only see the BACKUP.DS structure but see how the NDS files are located within
it.

                 . o O o . [ BACKUP.DS Structure ] . o O o .

If you retrieve BACKUP.DS, you need to reconstruct the NDS files into their
original 4 components. The structure of BACKUP.DS is as follows -

  |---------------|
  |    HEADER     | <------- Header.
  |---------------|
  | Offset to end | <------- This offset is located just past the header.
  | of ENTRY.NDS  |          (unsigned long int)n = Offset/sizeof(ENTRY)
  |---------------|          gets the number of records.
  |   ENTRY.NDS   | 
  |---------------|
  | Offset to end | <------- (unsigned long int)n = Offset/sizeof(VALUE)
  | of VALUE.NDS  |          gets the number of records.
  |---------------|
  |   VALUE.NDS   | 
  |---------------|
  | Offset to end | <------- (unsigned long int)n = Offset/sizeof(BLOCK)
  | of BLOCK.NDS  |          gets the number of records.
  |---------------|
  |   BLOCK.NDS   |
  |---------------|
  | Offset to end | <------- (unsigned long int)n = Offset/sizeof(PARTITIO)
  |of PARTITIO.NDS|          gets the number of records.
  |---------------|
  | PARTITIO.NDS  |
  |---------------|

The BACKUP.DS header section has a rather odd structure consisting of variable 
data. While I have taken a look at it, it is not that important when searching 
for NDS files. It does contain the server and tree name, and in multi-server 
environments you will get a copy of the SAP table, or at least something with a
lot of the same information. This is there, I assume, so that if and/or when 
you recover NDS your server will have a fairly clear view of what the network 
last looked like.

From this information you should be able to write a program in C that will take
a BACKUP.DS file and make the 4 NDS files. If you do not know how (or do not 
have the time) to write such a program, use CONVERT.EXE from Pandora to 
recreate NDS files from BACKUP.DS.

                   . o O o . [ Fun and Games ] . o O o .

It is possible, starting with PARTITIO.NDS and its reference to ENTRY.NDS, to 
follow each cross reference and get a complete dump of NDS. I have not written 
such a utility, nor would I care to. But one of the things that interested me 
the first time I looked at BACKUP.DS in a hex viewer was that I could possibly 
get user passwords. Novell has said that this was impossible, and that Netware 
4.x uses a public key encryption scheme for authentication. However this did 
not stop me from exploring and learning about the size and structure of 
individual pieces and attributes. The details of WHAT can be found are much 
more interesting than how I discovered the location of the private key. So 
what follows is the process to get a private key.

The logical thing is to determine what ENTRY.classID value represents a user 
object. This is fairly easy to determine by examining ENTRY records until a 
user record is found (CN=Admin makes it easy to find). However this may not be 
consistent between servers. Although I found that the classID for users is 
usually 0x010000af, in one case I found this value to be different. So to 
ensure that you have found the correct classID, search all of ENTRY.NDS looking
for objects with the name of "User". If there are two, the last one is the 
correct one. This particular object's classID is the ID that says an ENTRY.NDS 
record is a user record.

All objects are stored this way in NDS, so the next thing to do is determine
the classID associated with the Private Key object. This can be done by
searching for "Priv". Typically this is 0x01000045, although in one case I
found it was not. Therefore it is important to check to make sure.

To simplify searching of NDS records, all "system" related objects, such as the
User object and Private Key object, all have classIDs of 0xffffffff. To quicken
the search, simply look for ENTRY.NDS records with a classID of 0xffffffff and 
a name of "User" or "Priv" and you will have the object ID at ENTRY.id needed 
for later searching.

Let's say that we have searched ENTRY.NDS and retrieved 0x010000af for User
objects and 0x01000045 for Private Key objects. To retrieve user and password
info from NDS, we first need to search ENTRY.NDS for records with a classID of 
0x010000af. This will get us all of the user records, and by dumping ENTRY.name
you will have the Common Name (CN) of each user. Getting the ENTRY.id will help
us in the next step.

Searching each VALUE.NDS record for the ENTRY.id in VALUE.entryID and
0x01000045 in VALUE.typeID will get us the start of the Private Key.

The private key's size=324, so you will have to follow the VALUE.firstBlock
offset in BLOCK.NDS to retrieve all of the data. Here is an example of the 
first several bytes of a private key:

User "Simple" (object ID=090000c3) private key:
c3 00 00 09 08 00 00 00 8d 49 e4 55 f2 5a 57 0e bb 8a f9 f6 ea 3a f6 88
01 00 00 00 01 00 60 00 20 01 19 01 38 74 08 89 02 c4 97 a3 43 5d 00 86
etc...

Comparing this private key to other private keys, the structure is roughly as 
follows:

typedef struct PRIV_KEY
{
	uint32		objectID;  /* Object ID of user. */
	uint16		pwLength;  /* Password length. */
	uint16		var1;      /* Unsure. Perhaps unused. */
	uint8		pw[16];    /* The password, one way hashed. */
	uint32          var2;      /* No idea, is always 0x00000001 */
	uint16		var3;      /* No idea, is always 0x0001 */
	uint16		var4;      /* No idea, is always 0x0006 */
	uint32		var5;      /* No idea, is always 0x01190120 */
	uint8		key[288];  /* The private key, encrypted. */
	uint32		var6;      /* No idea... */
	uint32		var7;      /* No idea... */
	uint32		var8;      /* No idea... */
	uint32		var9;      /* No idea... */
} PRIV_KEY; /* size=340 */

While it is interesting that on occassion you will see var8 and var9 equal the 
first four characters of the user's common name in unicode, the thing that 
really looks interesting is the one way encrypted hash. From studying bindery 
files it is known that the Netware 3.x password is this exact same length. By 
using the Netware 3.x algorithm that has been floating around on the Internet 
for a couple of years it turns out that YES this is the one way hash.

Retrieving this info from ENTRY.NDS, VALUE.NDS, and BLOCK.NDS to specifically 
grab info for password cracking could be done by simply looking for the various
IDs at the various offsets and writing them out to a file. To simplify this 
process, you could use EXTRACT.EXE from Pandora. EXTRACT.EXE creates an 
NDS-style password file.


                . o O o . [ Security Implications ] . o O o .

If you now have the user's common name, the object ID, the one way hash, and 
the algorithm, it should simply be a matter of time to crack the password. The 
algorithm used by Novell requires the length of the password, the password 
itself, and the object ID to generate the one way hash. Therefore a dictionary 
attack could be quite fruitful.

A hard-to-guess password will slow down the process of cracking the password 
considerably. However, the algorithm converts the password to upper case before 
encryption, and we know the password length before we even start cracking. 
While brute force attacks can be lengthy and tiresome, such an attack is not 
entirely out of the question.

To demonstrate this, you can use CRYPTO.EXE and CRYPTO2.EXE from Pandora to 
perform brute force and dictionary attacks respectively on the password file 
created from EXTRACT.EXE.

New to version 2.0 of Pandora are some improvements on this one way hash
algorithm. Jitsu-Disk's work in this area is documented in the accompanying 
paper (called CRYPT.TXT), and is based off of looking at crypto.c from the first
version of Pandora.

Other security implications include the fact that there are a number of older 
bindery calls intended for Netware 3.x that may be used against Netware 4.x, 
and these bindery calls could allow certain security measures to be 
circumvented.

While verifying that the same algorithm from Netware 3.x is being used for
Netware 4.x, it was discovered that by performing a verify password call you 
could dictionary or brute force attack an account on a Netware 4.x server. By 
default, intruder detection is turned off on a freshly loaded Netware 4.x 
server. If bindery emulation is being used, a "verify password" call can be 
used to check whether a user account exists, and whether a guessed password is 
correct.

A number of examples of programs used for logging in can adapted over to 
perform this type of attack. It has been reported that KNOCK.EXE has been used 
by some hackers to gain access to a Netware 4.x server, and I am aware of one 
development company of Netware utilities that had discovered the same thing.

New with version 2.0 is INTRUDE.EXE and SUPE.EXE. Both are designed to take 
advantage of "verify password" routine. INTRUDE.EXE actually does several 
things -- it can take a text file containing possible account names on the 
Netware 4.x user and test each one individually to see if it is a valid name, 
during the valid name check it will also check to see if the valid name has no 
password, and it can perform a dictionary attack against a selected account 
name. SUPE.EXE also does a dictionary attack against a Netware 4.x server, but 
it acts only against a "hidden" object.

The reason this works is because the very first object created by NDS is a 
bindery-style object called Supervisor -- the "hidden" object. This account has 
full access to the file system of the server. But because it is not a regular 
NDS user object, it does not show up under any of Novell's regular user 
utilities. In Netware 4.1 (loaded without newer version of NDS from Novell's 
web site) there is no property that allows Intruder Detection to work on this 
object, nor is there any of the normal enforcement of password attributes such 
as minimum length for a password. Therefore by using normal 3.x bindery-style 
calls the Supervisor account can be attacked.

Later versions of NDS (available from Novell's website) and the shipping 
versions of Netware 4.11 give this Supervisor object the password attributes
and the ability to perform Intruder Detection. But since Intruder Detection is 
off by default, utilities such as SUPE.EXE could succeed. Once the password has
been obtained, this makes for an excellent backdoor into the server. This 
object and its use go undetected, although you can only access that particular 
server. But since NDS is distributed among servers you can access any server by 
logging out and back in to one of the other servers.

One further note, since the calls are "verify password" calls, you can be 
logged in as GUEST and then use INTRUDE.EXE or SUPE.EXE without having to log 
out first.

                       . o O o . [ Summary ] . o O o .

Pandora can be used by an intruder (or an adminstrator) in the following
fashion to break in (or to determine vulnerability):

 - Use INTRUDE.EXE to determine common user accounts. 

 - Use INTRUDE.EXE to determine passwords. This is a dictionary attack, 
   and any account can be attacked. 

 - Alternately SUPE.EXE can be used to determine the password to the 
   special Supervisor object.

 - If SUPE.EXE and your dictionary list cannot find the password for
   Supervisor, try using KNOCK.EXE if Intruder Detection was not triggered.

 - By exploiting the information collected from INTRUDE.EXE, SUPE.EXE, or 
   any other utility, try to access SYS:SYSTEM.

 - If BACKUP.DS exists, it can be copied off of the server.

 - By exploring the NCF files it should be possible to determine the remote
   console password, or possibly exploit the read/write access to an NCF 
   file to gain console access.

 - Once console access is gained, using Novell's DSMAINT a fresh BACKUP.DS
   can be created and copied down.

 - BACKUP.DS can be converted into the original NDS files using CONVERT.EXE.

 - The NDS files can have EXTRACT.EXE run against them to create the
   PASSWORD.NDS file.

 - CRYPTO.EXE or CRYPTO2.EXE can be run against PASSWORD.NDS to do either a  
   brute force attack or a dictionary attack to obtain additional passwords. 
   For the intruder, this is safer since these attacks can occur offline.

 - Odds are that accounts and passwords obtained also exist on other 
   platforms (such as NT, Unix, MVS, etc.), eliminating the need to "crack" 
   those platforms.

                  . o O o . . . . . . . . . . . . o O o .


                Simple Nomad - Nomad Mobile Research Centre
                 <thegnome@nmrc.org> - http://www.nmrc.org/
          Original June 27, 1997 . o O o . Revised March 10, 1998
