5 **category**: rev, (web)
9 Most people just give you a present for christmas, hxp gives you a glorious future.
11 If you’re confused, simply extract the flag from this 山葵 and you shall understand. :)
17 As previous experience shows, it is always best to read the description very carefully and look for clues! Of course, the Asian characters stand out: Google Translate quickly tells us that they mean "wasabi" (in Japanese), the Japanese hot sauce/paste. If we search for "wasabi web framework" on Google, the first site is a Kotlin based HTTP framework [https://github.com/wasabifx/wasabi], for now that does not seem particularly interesting or valuable. On the other hand, the second page takes us to [https://github.com/danleh/wasabi], an analysis framework for the inspection of WebAssembly files. This sounds more relevant as we are in fact given a .wasm file. More on this later.
19 We are given the skeleton of a php web server. Among others we are presented with:
27 With the provided shell script (run.sh) we are quickly up and running our PHP based web server.
29 ![page.png](hxp36c3/page.png)
31 We have a funky Matrix inspired flag checker tool. As the page advertises, it is supposed to be running Rust and WebAssembly. Let's check the underlying structure and how all this comes together!
33 The interesting part of the HTML source looks like this:
37 <div id="inner-container">
38 <h1>High-speed flag checking</h1>
39 <h3>Powered by Rust <img src="./ferris.svg" /> and WASM <img src="./wasm.svg" /></h3>
42 <input type="text" id="flag" placeholder="hxp{...}" size="50" /><br />
43 <button type="submit">Check</button>
48 <script type="module">
49 import init, { check } from './hxp2019.js';
51 async function run() {
54 document.getElementById('form').addEventListener('submit', function(e) {
56 const flag = document.getElementById('flag').value;
58 alert('Yes, you found the flag!');
69 We see that there's a button submitting information that is checked by an other JavaScript function defined in the file `hxp2019.js` (reformatted for easier reading):
74 let WASM_VECTOR_LEN = 0;
76 let cachegetUint8Memory0 = null;
77 function getUint8Memory0()
79 if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer)
81 cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
84 return cachegetUint8Memory0;
87 let cachedTextEncoder = new TextEncoder('utf-8');
89 const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
90 ? function (arg, view)
92 return cachedTextEncoder.encodeInto(arg, view);
94 : function (arg, view)
96 const buf = cachedTextEncoder.encode(arg);
104 function passStringToWasm0(arg, malloc, realloc)
107 if (realloc === undefined)
109 const buf = cachedTextEncoder.encode(arg);
110 const ptr = malloc(buf.length);
111 getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
112 WASM_VECTOR_LEN = buf.length;
116 let len = arg.length;
117 let ptr = malloc(len);
119 const mem = getUint8Memory0();
123 for (; offset < len; offset++)
125 const code = arg.charCodeAt(offset);
126 if (code > 0x7F) break;
127 mem[ptr + offset] = code;
134 arg = arg.slice(offset);
136 ptr = realloc(ptr, len, len = offset + arg.length * 3);
137 const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
138 const ret = encodeString(arg, view);
140 offset += ret.written;
143 WASM_VECTOR_LEN = offset;
149 * @param {string} pwd
152 export function check(pwd)
154 var ptr0 = passStringToWasm0(pwd, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
155 var len0 = WASM_VECTOR_LEN;
156 var ret = wasm.check(ptr0, len0);
160 function init(module)
162 if (typeof module === 'undefined')
164 module = import.meta.url.replace(/\.js$/, '_bg.wasm');
169 if ((typeof URL === 'function' && module instanceof URL) || typeof module === 'string' || (typeof Request === 'function' && module instanceof Request))
172 const response = fetch(module);
173 if (typeof WebAssembly.instantiateStreaming === 'function') {
174 result = WebAssembly.instantiateStreaming(response, imports)
178 if (r.headers.get('Content-Type') != 'application/wasm') {
179 console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
180 return r.arrayBuffer();
185 .then(bytes => WebAssembly.instantiate(bytes, imports));
190 .then(r => r.arrayBuffer())
191 .then(bytes => WebAssembly.instantiate(bytes, imports));
196 result = WebAssembly.instantiate(module, imports)
198 if (result instanceof WebAssembly.Instance) {
199 return { instance: result, module };
205 return result.then(({instance, module}) => {
206 wasm = instance.exports;
207 init.__wbindgen_wasm_module = module;
217 If we take a closer look at the definition of the function "check", we see that it passes a string to the function "passStringToWasm0", then it passes this processed string to the .wasm binary. The string-processing function takes a string and copies it to a prepared area of memory. Probably this area is used for communication between the binary and the JavaScript function.
219 Let's take a look at the binary with our new tool! I set up wasabi and completed all the steps on their website, but as to how to actually use their software, I found no description. After trying around for a few hours and finally giving up, I moved on to wabt[https://github.com/WebAssembly/wabt].
221 I managed to disassemble the .wasm file with the wabt module wasm2c. It returned me a very strange looking 6000 lines long C-file and a short but unusual header file. After some searching around I have found the definition of the check function called by the JavaScript file:
224 static u32 check(u32 p0, u32 p1) {
239 alloc__vec__Vec_T___from_raw_parts__h6aeafb6342a4f3ed(i0, i1, i2, i3);
246 alloc__vec__Vec_T___into_boxed_slice__h0afc7190c9c73a6d(i0, i1);
248 i0 = i32_load((&memory), (u64)(i0 + 8));
251 i1 = i32_load((&memory), (u64)(i1 + 12));
253 i0 = hxp2019__check__h578f31d490e10a31(i0, i1);
261 __rust_dealloc(i0, i1, i2);
273 I was not able to guess what the code does exactly, but there is a suspicious looking function being called: `hxp2019__check__h578f31d490e10a31`. It's definition is similarly fuzzy, I did not include it due to its size 300+ lines. It can be found in the folder hxp36c3 under the filename: `hxp2019__check__h578f31d490e10a31.c`.
275 ### Technical details
277 WebAssembly was a new technology for me:
279 > WebAssembly is a new type of code that can be run in modern web browsers and provides new features and major gains in performance. It is not primarily intended to be written by hand, rather it is designed to be an effective compilation target for low-level source languages like C, C++, Rust, etc.
281 [https://developer.mozilla.org/en-US/docs/WebAssembly/Concepts]
283 So it is a binary, that either resides server- or client-side, and provides "near native speed" functionality to JavaScript applications. After some research on the security implications of WebAssembly, I have found the following potential issues:
285 The sandboxed environment it runs in has complete control over what the binary can access.
287 > There currently is no way to do integrity checking on Wasm applications. This means that there is no process for verifying that a Wasm application has not been tampered with.
289 [https://www.forcepoint.com/blog/x-labs/webassembly-potentials-and-pitfalls]
291 The lack of integrity checking could open up a lot of ways for various MITM attacks.
293 ### Lessons (to be) learned
295 - Tools for unconventional SQL injection
309 We get a netcat service, we connect to it, we are presented with a 12 character hex value. After it, we have approximately 2 minutes to provide the correct "answer".
311 We are also presented with python file that :
319 assert tuple(map(len, (key, blk))) == (9,6)
320 S = lambda j,v: (v << j | (v&0xffffff) >> 24-j)
321 ws = blk[:3],blk[3:], key[:3],key[3:6],key[6:]
322 x,y, l1,l0,k0 = (int.from_bytes(w,'big') for w in ws)
325 l.append(S(16,l[i]) + k[i] ^ i)
326 k.append(S( 3,k[i]) ^ l[-1])
328 x = S(16,x) + y ^ k[i]
330 x,y = (z&0xffffff for z in (x,y))
331 return b''.join(z.to_bytes(3,'big') for z in (x,y))
333 # did I implement this correctly?
334 assert Speck(*map(bytes.fromhex, ('1211100a0908020100', '20796c6c6172'))) == b'\xc0\x49\xa5\x38\x5a\xdc'
338 v = m + bytes(-len(m) % 9) + len(m).to_bytes(9,'big')
339 for i in range(0,len(v),9):
340 s = Speck(v[i:i+9], s)
349 s = bytes.fromhex(input())
351 print('The flag is: {}'.format(open('flag.txt').read().strip()))
356 We recognize some kind of unusual encryption scheme, which is unidirectional, only the way of encryption is provided to us. The main function used for encryption is called "Speck", which is equivalent of bacon in German. This seems interesting! After some googling around we find the Speck cipher (see TD).
360 assert tuple(map(len, (key, blk))) == (9,6)
361 S = lambda j,v: (v << j | (v&0xffffff) >> 24-j)
362 ws = blk[:3],blk[3:], key[:3],key[3:6],key[6:]
363 x,y, l1,l0,k0 = (int.from_bytes(w,'big') for w in ws)
366 l.append(S(16,l[i]) + k[i] ^ i)
367 k.append(S( 3,k[i]) ^ l[-1])
369 x = S(16,x) + y ^ k[i]
371 x,y = (z&0xffffff for z in (x,y))
372 return b''.join(z.to_bytes(3,'big') for z in (x,y))
375 Here is a variant that's a bit easier to read, let's compare!
376 [https://en.wikipedia.org/wiki/Speck_(cipher)]:
379 #define ROR(x, r) ((x >> r) | (x << (64 - r)))
380 #define ROL(x, r) ((x << r) | (x >> (64 - r)))
381 #define R(x, y, k) (x = ROR(x, 8), x += y, x ^= k, y = ROL(y, 3), y ^= x)
384 void encrypt(uint64_t ct[2],
385 uint64_t const pt[2],
388 uint64_t y = pt[0], x = pt[1], b = K[0], a = K[1];
391 for (int i = 0; i < ROUNDS - 1; i++) {
401 The first thing that stands out, is the fixed number of rounds in our variant. 22 rounds mean that our key size is 64 or 72 bits. This in turn means that our block size is 32 or 48 bits.
403 The assignment `s = bytes(6)` from the function "H" reveals us that we have to do with the variant where the block size is 48 bits.
405 `assert tuple(map(len, (key, blk))) == (9,6)` This assertion makes sure that we are using the correct key and block sizes.
407 The main loop seems to behave according to the cipher specification and the cor of the example C code:
411 x = S(16,x) + y ^ k[i]
415 The key is XORed into the left word (y) then the left word is XORed into the right word (x).
417 I have felt here, that my cryptanalysis skills are not yet sufficient enough, so I looked at the other function, maybe I can find some way to brute force Speck.
422 v = m + bytes(-len(m) % 9) + len(m).to_bytes(9,'big')
423 for i in range(0,len(v),9):
424 s = Speck(v[i:i+9], s)
428 Here I made the revelation that our Speck function is actually only one "step" of the calculation! Speck is a block cipher:
431 for i in range(0,len(v),9):
432 s = Speck(v[i:i+9], s)
435 this loop goes through the message block-by-block. So actually `H` is an implementation of the complete 72/48 Speck encryption scheme. Now we know what precisely the code does, we can try to brute force it!
437 After some trying around, I realised that there is no chance, that consumer-grade hardware is able to brute-force this encryption under 2 minutes, not even 2 hours.
439 ### Technical details
441 > Speck is a family of lightweight block ciphers publicly released by the National Security Agency (NSA) in June 2013. [...] a cipher that would operate well on a diverse collection of Internet of Things devices while maintaining an acceptable level of security.
443 [https://en.wikipedia.org/wiki/Speck_(cipher)]
445 I think the effectiveness of the cipher comes from the fact that is uses lower key and block sizes when compared to other ciphers, such as AES.
447 > As of 2018, no successful attack on full-round Speck of any variant is known. [...] they [the NSA] found differential attacks to be the limiting attacks, i.e. the type of attack that makes it through the most rounds; they then set the number of rounds to leave a security margin similar to AES-128's at approximately 30%.
449 [https://en.wikipedia.org/wiki/Speck_(cipher)]
451 So, according to the NSA, the Speck cipher is as resilient against differential attacks as AES-128.
453 ### Lessons (to be) learned
455 - Applied cryptanalysis?
456 - Brute forcing through the use of clusters?
464 Finally (again), a minimalistic, open-source file hosting solution.
470 We get a dockerfile, with which we try to set up as a webserver. No matter how we try, the build command returns an error because the file `flag.txt` is (understandably) not found. After some trying around and understanding the problem, I was able to start the container just by creating the mentioned file in the directory.
472 The service provided is a barebones website where we can upload small files, I have noticed (initially) that files over 500kB get rejected. The accepted files are then assigned links with their serial number. The file type is also displayed.
474 ![page2.png](hxp36c3/page2.png)
476 We grab the webserver banner with `nmap`:
479 8000/tcp open http nginx 1.14.2
480 |_http-server-header: nginx/1.14.2
483 We have an nginx server running!
485 In the provided files, apart from the Dockerfile we have a .php index file too,
491 ini_set('display_errors', 0);
492 ini_set('display_startup_errors', 0);
495 if( ! isset($_SESSION['id'])) {
496 $_SESSION['id'] = bin2hex(random_bytes(32));
499 $d = '/var/www/html/files/'.$_SESSION['id'] . '/';
500 @mkdir($d, 0700, TRUE);
501 chdir($d) || die('chdir');
503 $db = new PDO('sqlite:' . $d . 'db.sqlite3');
504 $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
505 $db->exec('CREATE TABLE IF NOT EXISTS upload(id INTEGER PRIMARY KEY, info TEXT);');
507 if (isset($_FILES['file']) && $_FILES['file']['size'] < 10*1024 ){
508 $s = "INSERT INTO upload(info) VALUES ('" .(new finfo)->file($_FILES['file']['tmp_name']). " ');";
510 move_uploaded_file( $_FILES['file']['tmp_name'], $d . $db->lastInsertId()) || die('move_upload_file');
514 $sql = 'SELECT * FROM upload';
515 foreach ($db->query($sql) as $row) {
516 $uploads[] = [$row['id'], $row['info']];
521 The server is assigning a unique PHP session variable to each user, files are identified by being placed in a folder with the respective session id. The server uses a sqlite3 database and the connection is handled by the PHP engine. We also discover, that the file size limit is not 500kB but only 10kB! `$_FILES['file']['size'] < 10*1024`
523 The queries used seem pretty basic, for now, I do not see any countermeasures against sql-injection. This command seems especially vulnerable:
526 "INSERT INTO upload(info) VALUES ('" .(new finfo)->file($_FILES['file']['tmp_name']). " ');"
529 After some reading around online, it turns out, file upload vulnerabilities are not performed through the filename(what I would have expected), but through image metadata. The sanitization of said metadata makes this possible.
531 Apparently, traditional tools, like `sqlmap` are not capable of such attacks.
533 Unfortunately, I could not find any tools online, which are able to perform an sql injection attack from such an unusual attack vector.
535 ### Technical details
537 I have found an explanation for the vulnerability, it is an RCE:
539 > I discovered a technique to hide php code in the EXIF data of the image file. When the image is loaded by the page, the php tags located in the headers are interpreted as php code and run by the server.
541 [https://spencerdodd.github.io/2017/03/05/dvwa_file_upload/]
543 When PHP handles images, it is possible to escape processing from inside EXIF headers and run instructions. The vulnerability lies in the fact, that PHP tags are recognized in areas where it should not be possible, such as image metadata.
545 ### Lessons (to be) learned
547 - SQL injection is not only possible through GET and POST parameters
548 - The use of more advanced SQL injection tools