Finding DES Encryption Known Plaintext Attack in the Wild

Finding DES Encryption Known Plaintext Attack in the Wild


7 min read

\x00 - TLDR;

In this article I will follow the steps required to analyze a cryptographic algorithm type, identify the algorithm, and perform known plaintext attack in order to expose the DES encryption keys.

\x01 - Little Bit of Theory

Stream Ciphers vs. Block Ciphers 101

Prerequisite - basic understanding of the differences between symmetric and asymmetric encryption.

Both stream cipher and block cipher are from the family of symmetric encryption.
The main difference is how they handle the plain text for encryption:

  • Stream Ciphers encrypt the plaintext bit by bit, resulting in the following behaviors:

    • The key XORs the plaintext's binary format.

    • The ciphertext output will be somewhat long as the plaintext input.

    • There is no fixed length of ciphertext output because the padding is unnecessary.

  • Block Ciphers encrypt the plaintext by chunks (blocks), resulting in the following behaviors:

    • Each block of plaintext is encrypted separately (in some block algorithms the blocks are independent and in some, each block is used to encrypt the next block).

    • The output will have a fixed length depending on the number of blocks being generated.

    • If plaintext is not enough to fill a whole block, the algorithm will use padding to fill the gap.

For Visualization's sake, two images that shows how a stream cipher behaves while the plain text has 16 and 17 characters input.

The differences between the two ciphertext outputs are only "17" HEX char.

And the same test in block cipher will give the following results:

Notice how a whole new block of characters was added while adding another char, it is because we've started a new block, and padding was needed.

DES Algorithm 101

DES stands for Data Encryption Standard and is a type of symmetric-key encryption.
DES is also deterministic encryption, which means the same plaintext input will produce the same ciphertext output (given the same key of course).
DES works with a block size of 64 bits, and 64 bits key-length but actually only 56 bits are being used!
Meaning, that DES can happen to have multiple keys that result in the same ciphertext, as every 8th bit is discarded! How amazing is that?

Different keys same result!

Now we got all we need to see how we can break DES encryption in the wild.

For more information about DES encryption, here is a good detailed resource:

\x02 - Finding DES in Practice

Demo App

Let's say we have some demo app with funny login (I am lazy to write a demo app with DB access so please don't judge me), that each username we will submit - the application gives us back an encrypted session with the username inside.

Our job is to get admin access to the application, but the mighty devs already thought of that and implemented a security check to prevent us from doing it!

const express = require('express');
const crypto = require('node:crypto');
const app = express();
var bodyParser = require('body-parser')
const cookieParser = require("cookie-parser");

app.use(bodyParser.urlencoded({extended: true})) 

// create a route for handling user input'/test', (req,res,next) => {
})'/user', (req, res) => {

  const user = req.body.user;

  //super usefull anti-hackers filter - no admin for you sir
  if (user.toLowerCase().includes("admin")){
    res.send("Evil hackers are not welcome here!");

    // encrypt the user parameter using DES encryption
    const iv = new Int8Array(8);
    const cipher = crypto.createCipheriv('des', 'secret1!',iv);
    let encrypted = cipher.update(`user:${user}`, 'utf8', 'hex');
    encrypted +='hex');

    // set the session cookie with the encrypted user parameter
    res.cookie('session', encrypted);

    res.send('Session cookie set with encrypted user parameter.');

// a route for checking the admin user
app.get('/admin', (req, res) => {
  // get the encrypted session cookie
  const encrypted = req.cookies.session;

  // decrypt the session cookie
  const iv = new Int8Array(8);
  const decipher = crypto.createDecipheriv('des', 'secret1!', iv);
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted +='utf8');
  var userDec = decrypted.substring(5);

  // check if the decrypted cookie value contains the "admin" user
  if (userDec == 'admin') {
    res.send('Logged in as Admin! Congratz :)');
  } else {
    res.send(`No admin no cookies! Go away ${userDec}`);

// start the server
app.listen(3000, () => {
  console.log('Server listening on port 3000');

Notice that here for the sake of visibility and easiness I made the encrypted cookie output in HEX format, usually, it will be in Base64 format.

Generating an encrypted cookie.

Trying to get to the admin panel (and failing of course).

Mapping the Algorithm - Stream vs. Block

Let's say we don't have the actual source code, how do we know which type of encryption is implemented here?

As described earlier - stream ciphers grow accordingly to the plaintext, while block cipher adds a whole new block with padding as the plaintext gets bigger.

If "a" and "aa" will result in output with some characters added - we have a chance for stream cipher, if we get a whole block it's a block cipher.

bbdfe2aa9b910228 and 31e52e2d43416dec have the same length, meaning we are probably facing a block cipher here.
Now let's look at what will happen if we add another "a" to the input:

Whoa! A whole new block was added! This looks like a block cipher and leads us to the next analysis stage.

Mapping the Algorithm - Find the Algorithm

DES has a block size of 64 bits/ 8 characters, meaning every 8th character of the ciphertext will create a new block (full of padding + one "a" char).
While other encryption algorithms have different/larger block sizes (such as AES with 128 bits block sizes).

So by measuring the newly created block size we can deduce the encryption algorithm!

As we don't know (yet) the exact position of our input inside the ciphertext, it is mandatory to add multiple "a" until noticing that some parts of the ciphertext are staying constant (with that we are taking advantage of the deterministic character of the DES algorithm).

aa = bbdfe2aa9b910228

aaa = ba2a9c5227c7f473 e20b8d88d3e0400b

aaaaaaaaaaa = ba2a9c5227c7f473 bb9f674c52fffad7 2969f0022c021881

aaaaaaaaaaaaaaaaaaa = ba2a9c5227c7f473 bb9f674c52fffad7 89305a3db15883d202b4d2dd41cb3083

As we add input we notice more and more parts are being constant, counting the characters we can see that we have an 8-character block (16 in HEX), meaning 64 bits.

It is important to note that when concluding it is DES and not 3DES for the sake of the article, there is no way to tell which one of the algorithms that use 64 bits block size we are facing, luck is - there are not many of those (and 3des also has known plaintext attack) :)

Performing A Known Plaintext Attack

Now we have (almost) all we need to perform a known-plaintext attack.
To successfully pull it off we need to somewhat guess/deduce the following:

  1. Where our input is located inside the ciphertext's first block?

  2. What is the rest of the text inside the first block?

I have not talked about encryption modes in this article because I'm trying to avoid writing way-too-long articles.
It is important to note that different encryption modes can make our life harder/easier depending on the mode userd.
In ECB for example it is not needed to guess anything, as any block full of "a"s from the middle of the ciphertext is enough, but here its not the case.
More on encryption mode can be found here:

From previous inputs, we know that "aaa" starts the new block - so before it, we have only 5 characters.
From the function context, we know we are dealing with a username input field, and probably this is what's being encrypted.
Most of the time a delimiter is being used, such as ":" or "=", and a good chance our cookie can be either plain text or JSON object, or XML tag - as they are easy to parse.

Let's craft a list of potential plaintexts for the first block:

  1. <usr:aaa

  2. <usr=aaa

  3. {usr:aaa

  4. {usr=aaa (long shot but you never know...)

  5. user=aaa

  6. user:aaa

We will create a file with the plaintext and cipher text in the following format hex(ciphertext):hex(plaintext) to pass to Hashcat:


hashcat -a 3 -m 14000 usernameDES.hash -1 /usr/share/hashcat/charsets/DES_full.hcchr --hex-charset ?1?1?1?1?1?1?1?1

It can take a while, but in the end, if guessed right - the secret key will be exposed.
Then all that's left is to encrypt the following value: "user:admin" and submit it to the admin endpoint.

Seems too much of a CTF, but this issue was found on a real app (with different parameters thou) so from now on don't forget to check any crypto stuff out there!