Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
There are two vulns here. A bypass to the JSON validation, and a JWT signing vulnerability. The first part is pretty simple. The app uses '"jwt-simple": "0.5.2"', which has a vulnerability allowing us to bypass the pattern validation, allowing us to gain a JWT token for a 'low privilege' user. r = requests.post(url + "checkin", json={"firstName": "a", "lastName": "b", "passport": "123456789", "ffp": "CA12345678", "extras": {"sssr" : {"sssr": "FQTU"}, "constructor": {"name":"Array"}} })
The JWT library is vulnerable to being signed as HMAC with the public key, a common vuln. However, we have no pubkey. I was stuck for ages, until I spotted (thanks Makelaris) https://blog.silentsignal.eu/2021/02/08/abusing-jwt-public-keys-without-the-public-key/ This provided a Docker image where I could simply provide two JWT's, modify the payload in the program and run it. It pulled out the paramaters, and generated 4 potential public keys, signing my data with each. Using this:
We get the flag:
union{I_<3_JS0N_4nD_th1ngs_wr4pp3d_in_JS0N}`
The script implements a discrete log function using matrices. We solve this by doing some stuff with the Jordan form of the matrices by using this stackexchange post and yada yada yada oh hey whats this anyway yoink script bam solve
Server implements ECDSA, and allows us to send what we want to Alice and Bob. We can also control a nonce, which gets xored with the private key at the end. The server only does one check, and that is that the private keys match at the end of the transfer. We can send the generator point to both Alice and Bob, as we know what the generator multiplied by their private keys are, and then we create a pair of nonces so that they xor to give the same private key. We then just decrypt the flag by using AliceX ^ BobNonce as the shared secret
There are two vulnerabilities - one in create, one in append. Show and delete are perfectly safe.
You may notice there are two different lengths or sizes the program takes in when creating - content_length and size. To grab your contents data, it uses the read_contents function
As you can see, it uses kmalloc to get a page from the kernel with content_length being the size, and uses copy_from_user to copy into it. Nothing vulnerable here specifically, however
The program kmallocs a new page for the nut contents, but this one's size is the size parameter. Furthermore, it memcpy's size bytes into it. The contents pointer from read_contents is from a chunk of size content_length, but size bytes are copied from it. If size is greater than content_length, that's an out of bounds read. We will use this later to leak.
In the create function, the size is validated before usage. In the append function, the new size calculated is validated, however the size of the data you are appending is not. This doesn't seem consequential, but due to an error in the read_size function, it is.
The program doesn't check if the size outputted from read_size is -EOVERFLOW. Which means, if we send a size such that it is less than 0 or greater than/equal to 1024, it will return -EOVERFLOW as the size. This will be added to the original size of the nut we are appending to, giving the new size.
EOVERFLOW is 75 in the generic linux implementation. This means -75 will be added to get the new size - the new size will be smaller than the old size. Since all of the original data will be copied, and then some from our own contents, this gives oodles of overflow. More specifically - an out of bounds write. More specifically, we get 75 bytes of overflow from the original nut, and an additional -75 & 0x3ff(0x3b5, because of how memcpy_safe ANDs the difference with 0x3ff)
If we, for example, send a content_length of 0x20 and a size of 0x60, 0x40 bytes outside of the first contents chunk will be read. I used this to find a list of useful kernel structures.
In kmalloc-32, we can cause a seq_operations struct to be allocated by opening a /proc/self/stat file. Said structures are freed when we close the file. What we'll want is for the allocation is to have a seq_operations struct right after it. We can achive this by allocating 20 structs(for good measure) and freeing every other one.
We don't actually need this, but for good measure, the kernel heap can also be leaked. msg_msg structures are very easy to allocate, free and control the size of, making them invaluable for heap spraying. They can be any size from 0x31(controlled by message length) - I decided to target kmalloc-92. For this, a message length of 48 is needed.
We can run msgget, store the qid, and then msgsnd to allocate a msg_msg structure. The beginning of the msg_msg structure has a pointer to the previous structure, so we'll need 2 with a free chunk inbetween.
Now for the final part - gaining the write.
Dumping the kmalloc-92 slab, we see the freelist pointers are in a normal order, and are not hardened. Its a singly linked list too, meaning with the overflow we can overwrite a pointer to force kmalloc to return a chunk wherever we want.
I spammed a few chunks to get rid of any possible free chunks in between other chunks. Then, I used pattern.py to generate a pattern which I dumped into the contents before appending. By appending to a chunk which is 0x60 + 75 in length, we get overflow on a chunk which is in kmalloc-92. When we allocate again with both sizes 0x60...
When trying to grab the next pointer, it will reach a fault, attempting to access controlled pointer + 0x60. When the fault happens, we can subtract 0x60 and use pattern.py to find that the offset until the controlled pointer is 357.
Exploit path:
Now... we have a leak of the kernel base, so where to write?
The easiest place to write to is a symbol in the kernel called modprobe_path
. When the kernel tries to resolve how to run a file with an unknown header, it will call the modprobe binary. The path of the modprobe binary is stored in modprobe_path
. And, best of all, it'll be run as root.
\xff\xff\xff\xff
is pretty much guaranteed to be an unknown header. If we try to run a file consisting of this, the kernel will run modprobe as root. If we've overwritten modprobe_path with the path to a bash script which has some malicious commands.
I copied my modprobe_hax function from fizzbuzz's hashbrown writeup(https://www.willsroot.io/2021/02/dicectf-2021-hashbrown-writeup-from.html) and changed the commands to fit.
/home/user/w will copy the flag to the root directory and make it accessable to all users. All that is needed now is to set modprobe_path to /home/user/w.
After doing this, we can extract the flag.
Primes are generated by weird ECC thing, but it's over the field of QQ, meaning that p and q increase a lot as k increases. It is therefore just feasible to bruteforce k to find p and q
The binary does basically nothing in main. I had a look at __libc_gnu_init
(runs before main) and noticed it read /proc/self/cmdline
and does some XORing against a static value named GNU_HASH
. All I had to do was reverse the XOR and I got the 'desired cmdline':
#### Flag: union{ct0rs_b3war3}
The script implements the SIDH key exchange scheme. If we look into more detail, it gives us a bit more information: ϕ_A(P_A).xy(), ϕ_A(Q_A).xy() the kernel of ϕ_A is P_A + Q_A k_A, and so ϕ_A(P_A) + k_A ϕ_A(Q_A) = 0 this means we can solve for k_A, and since the order of the subgroup is 2^216, discrete log is easy to do
sage go brrr or something i need to study this more