Abusing DNS: Part 5, Client says what?

Abusing DNS: Part 5, Client says what?
Photo by Josep Castells / Unsplash

Now that our client has the ability to read arbitrary data from the server, lets update the client to be able it to send arbitrary data to the sever. (Psssst, if you are new start with Part1)

The problem

As we learned in Part4 DNS can only transfer very small chunks of data mostly from the server to the client. Worse yet there is nothing like a TXT record that lets us send bigger records. There is good news, we can ask the server about any domain name we would like.

Domain names

In DNS the maximum length of a domain is 253 characters. That is a good chunk of data, but there is another limitation. A domain looks something like this www.google.com. Each parts in between the .'s are called labels. Sadly, if we want to get to something works on the wider internet, we don't control all of the labels. For our purposes we are going to use two labels (more on this below). Which leaves us a maximum of 63 characters to work with. There is one more problem but we will talk about that soon.

The answer

Once again we are going to take advantage of being able to ask as many questions as we want, and this time we are also going to take advantage of being able to ask multiple types of questions. To send data we are going to encode our data and chunk it in AAAA record queries until we are done. Once the data is all sent we will send an A record query to let the server know we are done. All the server needs to do is append anything it gets as an AAAA record to the value and when it gets the A query it knows the value is ready.

Wait a minute, we only have 63 bytes of data to work with, how can we send an arbitrary key and value? If we just send AAAA records how will the server know what key to append the value to?

Well, smarty pants, I'm glad you asked. We are going take advantage of labels to make a session. Our DNS requests will be for a domain with two labels that looks like this: {ENCODED_DATA}.{SESSION_ID}

Then all we have to do is encode the key and the value into the data and we are good to go. Thankfully rust's type system and encoding is here to save us.

The answer redux

Okay that was a little hard to write, I have to imagine it is pretty hard to read. To rephrase it we are going to update our strategy as follows.

Client

  1. Pick a random u16 value as our session id
  2. Encode our data and send AAAA queries with a domain made of chunks of data
  3. Send a A query for the session id when we are done

Server

  1. Append all AAAA requests to the session value
  2. An A request means the client is done. Decode the message in the session and save the resulting message under the defined key.

Code updates

Okay up first, lets update the Message structure to have both the key and the value. In src/lib.rs update as follows:

struct Message {
  key: String,
  value: String
}

Structures do stuff

Client Updates

Now on the src/client.rs ... we are going to need to update the CLI parsing to take a new --set value which can parse the KEY and VALUE

...
.arg(
    Arg::new("set")
        .long("set")
        .num_args(2)
        .value_names(["KEY", "VALUE"])
        .help("Set the KEY to VALUE"),
)
.get_matches();
...

argument parsing

client --set SETME "This is the value to set"

now our cli client can do that ^

Have I mentioned that Clap makes parsing arguments pretty easy? Now let's define a new function.

async fn set_value(key: String, value: String){
 todo!()
}

Our new function

The new set_value function will take the key/value that the user wants to set and handle most of the logic to send data. Up first...

let id: u16 = rand::rng().random();
let domain = format!(".{id:x}");

create a session id

Here set_value makes up a random session value and it does that using rand. It will use the hex representation of the session as the domain. Remember the request will look something like {ENCODED_DATA}.{SESSION_ID} .

 let message = Message {
     key: key.to_string(),
     value: value.to_string(),
 };

create our simple message

Then set_value creates a new message with the key/value from the user. Followed by..

let blob = bincode::serialize(&message)?;
let encoded = BASE32_NOPAD.encode(&blob);

serialize/encode the message

Once again rust's ability to serialize and encode the messages to the rescue. In those simple two lines the `Message` structure is converted to a binary blog and then encoded in DNS domain safe BASE32, perfect for the next step. With the encoded data we can now chunk it up.

Chunk!

Okay not that chunk, we need to break the data up in to blocks of data.

for chunk in encoded.as_bytes().chunks(MAX_LABEL) {
    let chunk = String::from_utf8(chunk.to_vec()).unwrap();
    let fqdn = format!("{}{}", chunk, domain);
    let query = aaaa_query_record(&fqdn)?;
    sock.send_to(&query, server).await?;

chunk it up and send it off

Here set_value converts the encoded ASCII back into bytes with as_bytes, chunks those bytes into fun size pieces ready to send to the server as AAAA records. The chunk size is the MAX_LABEL accounting for the maximum label length of a DNS request.

let query = a_query_record(&format!("{id:x}"))?;
sock.send_to(&query, server).await?;

let the server know the client is done

Finally set_value will send an A query to the server for the just the session id letting the server know that message is done.

Server Updates

fn append_value(key: String, value: String) {
    let mut db = DATABASE
        .get()
        .expect("Database not initialized")
        .lock()
        .expect("Failed to lock database");
    let mut current_value = db.remove(&key).unwrap_or_default();
    current_value.push_str(&value);
    tracing::debug!("{} {}", key.clone(), current_value.clone());
    db.insert(key, current_value);
}

append values

The new append_value function takes a key and value. It appends whatever is passed as value to the value already in the database. Which makes the rest of the changes pretty simple. In our AAAA parser we add:

...
let name = q.qname.to_string().to_uppercase();
let parts: Vec<&str> = name.split(".").collect();
if parts.len() < 2 {
    tracing::debug!("Not enough parts!");
    // TODO: handle some errors
    return Err("That did not work".into());
}

append_value(parts[1].to_string(), parts[0].to_string());
...

This takes the query name, splits it by the . and uses the two values as arguments to the append_value function. Take note that the query looks like this VALUE.KEY and the split is 0 indexed (the only way I don't care what lua says) so part[1] is passed as the key and part[0] is passed as the value.

Now we just need to update parse_a_query

...
let key = q.qname.to_string().to_uppercase();
tracing::info!("new a query: {key}");
let value = get_value(&key).unwrap();

let decoded = BASE32_NOPAD.decode(value.as_bytes())?;
let msg: Message = bincode::deserialize(&decoded)?;

set_value(msg.key.clone().to_uppsercase(), value);
...

The server knows when it receives the A query the client is finished... Which means it can take the session from the query name, decode the data and store the value by the actual key name.

There is one gotcha to call out. You may have noticed when we lookup the query name we uppercase the value. That is because DNS is case insensitive and queries will often be dorked with by the DNS server chains. You may ask for whatthef.example.com but by the time it gets to the example.com servers the messages is something like wHaTTheF.eXamPle.com. That is one of the other reasons we are using BASE32_NOPAD to encode the data.

Have Fun!

That is it. We can now send arbitrary key/value data to the DNS server and retrieve it with the client! As with the other parts run cargo run --bin server in one terminal and we can have fun with the client in another terminal.

Now in the client run

# set the value
cargo run -q --bin client -- --set SUBSCRIBE "See protocols can be a lot of fun."

# get the value
cargo run -q --bin client -- --get SUBSCRIBE

Thanks for reading. If you are enjoying this please share, subscribe, yell, scream laugh. And of course I'm on bluesky and linkedin.

In the next installment we will finally set this thing up on the internet and get recursive queries happening. See you then.