This past weekend I played in the Defcon Qualification CTF and decided to do a quick write-up on one of the challenges I spent a fair amount of time solving. The challenge was named twentyfiveseventy and was located in the PWNABLE category, which typically means it contains some sort of vulnerability that can be exploited to gain access to a flag. After downloading the binary, I opened it up in IDA-Pro to start determining what this program did and what bugs might be present. After a little reverse engineering, I started recognizing what appeared to be the SNMP protocol being implemented in the application. This was confirmed when I realized the subtle hint of the challenge name referring to RFC 2570, the introduction of SNMP version 3.

 

Analysis of the program disassembly revealed that the binary actually loaded the flag for the challenge into memory upon startup. This led me to believe that typical exploitation of the binary was not likely the solution since the flag already resided somewhere in the application’s memory.

Further analysis revealed that the program would return the value of the flag if a properly authenticated and encrypted SNMP v3 GET packet was received requesting the correct OID.

 

My first thought in solving this problem was to try and locate an open source SNMP v3 library that could be used to create such a packet. However, I noticed that the challenge actually implemented SNMP in TCP rather than the traditional UDP. This meant most of the libraries I dug up required a good bit of rework to convert them to TCP. After a good bit of searching around, I remembered I had implemented an SNMP v3 library years ago in Java that might just work for this challenge.

 

This challenge looked like it was going to take a rather significant amount of programming, even with the library I had already implemented. I reviewed the decompiled parsing routine to see if any glaring bugs jumped out. From what I could see, the binary implemented SNMPv3 using MD5 as the hashing algorithm and DES_CBC for encryption. The following diagram shows the general structure of SNMPv3.

snmpv3format

http://www.tcpipguide.com/free/t_SNMPVersion3SNMPv3MessageFormat.htm

 

The challenge binary actually has a quite strict parser, checking the lengths of fields, their values, and validating the remaining number of bytes against previous header lengths. Many of these checks can be passed by sending what is typically referred to as a SNMP report and populating a new SNMP GET with its contents. An SNMP report is a packet that has the correct SNMP format but with most field values empty. The SNMP server will then send back an SNMP response with several of the fields populated. Some of these fields include the Engine Id, Engine Time, and Engine Boots.

 

There still remains a few fields in the protocol that we still have to determine in order to pass the checks in the parser. I found the username, engine id, and target OID in the data section of the binary.  They were “lbs”, “0x80007a6903deadbeefcafe”, and “1.3.6.1.2.1.1.5.0.0.0” respectively. The key fields left necessary to construct a correct packet are the authentication and encryption parameters.

 

Reviewing the function that sets the encryption key reveals that the key is actually randomly generated after setting the generator seed to the current time. The engine time is then set to this seed and leaked back to the client. As an added measure of integrity, the engine boots value is actually a number derived from the first randomly generated number which can be used to verify that the random number generator has been seeded correctly.

Setting the seed locally to the leaked engine time should allow us to generate the same random encryption key. Since my library was implemented in Java, I had to implement some JNA code to use the native libc rand function to ensure the algorithms were consistent.

 

interface CLibrary extends Library {
CLibrary INSTANCE = (CLibrary) Native.loadLibrary(“libc”, CLibrary.class);
void srand(int an_int);
int rand();
}
public static byte[] get_auth_key( long passedSeed, long engineBoots ){
byte[] retKey = new byte[32];

//Seed it
CLibrary.INSTANCE.srand((int)passedSeed);
//Throw away one
long tempNum = CLibrary.INSTANCE.rand();
long bootTest = (( tempNum >> 32 ) >> 25) + (( tempNum) & 0x7F )
– (( tempNum >> 32 ) >> 25);
if(engineBoots == bootTest){
//Create the encryption key
for( int i = 0; i < 32; i++){
long retVal = CLibrary.INSTANCE.rand();
retKey[i] = (byte) ((retVal % 94) + 32);
}
}

return retKey;
}

 

With the encryption key secured the only piece remaining was to figure out how to pass the authentication check. A teammate of mine spotted the solution to this dilemma. The parser only copies and validates the number of bytes of the auth md5 that is specified by the length value in the packet. This allows for the user to set the length to 1, which means there is a 1/256 chance that the first byte will match.

After putting everything together, I kicked off my Java SNMP client and after a few dozen requests I received the flag,  “Wait, one-byte password hashes aren’t secure? q@)$asllqkw4h5“.  The Java code is pretty large so if you’d like a copy feel free to send me a message.