Nestapp
Table of Contents
Overview
- 31 solves / 328 points
- Difficulty: Hardcore
- Overall difficulty for me (From 1-10 stars): ★★★★★★★★★★
Background
Author: Eteck#3426
In order to create an API with an auth system, a developer used NestJS. He tried to follows the doc and all the good practices on the official NestJS website, and used libraries that seems safe.
But is it enough ? Your goal is to read the flag, located in /home/flag.txt
Enumeration
Home page:
In here, we can login and create an account.
Let’s create an account!
After logged in, it renders: “As a regular user, you can’t do anything for now”
Hmm… Looks like we need to escalate our privilege to admin?
Burp Suite HTTP history:
When we’re registered or logged in, it’ll responses us a JWT (JSON Web Token) as the session cookie.
In the header, the JWT uses HS256 (HMAC + SHA256) algorithm. In the payload, we can see that there’s a pseudo
, sub
claim, it’s value is our username and user ID.
Then, it’ll also send a GET request to /infos
:
This will response us our username and user ID.
In this challenge, we can download the source code:
┌[siunam♥earth]-(~/ctf/PwnMe-2023-8-bits/Web/Nestapp)-[2023.05.07|16:51:00(HKT)]
└> file Nestapp.zip
Nestapp.zip: Zip archive data, at least v2.0 to extract, compression method=store
┌[siunam♥earth]-(~/ctf/PwnMe-2023-8-bits/Web/Nestapp)-[2023.05.07|16:51:01(HKT)]
└> unzip Nestapp.zip
Archive: Nestapp.zip
extracting: chall/.dockerignore
inflating: chall/.eslintrc.js
inflating: chall/.gitignore
inflating: chall/.prettierrc
inflating: chall/Dockerfile
creating: chall/front/
inflating: chall/front/index.html
inflating: chall/nest-cli.json
inflating: chall/package.json
inflating: chall/package-lock.json
inflating: chall/README.md
creating: chall/src/
inflating: chall/src/app.controller.ts
inflating: chall/src/app.module.ts
inflating: chall/src/app.service.ts
creating: chall/src/auth/
inflating: chall/src/auth/auth.module.ts
inflating: chall/src/auth/auth.service.ts
inflating: chall/src/auth/jwt.strategy.ts
inflating: chall/src/auth/jwt-auth.guard.ts
inflating: chall/src/main.ts
creating: chall/src/users/
creating: chall/src/users/dto/
inflating: chall/src/users/dto/create-user.dto.ts
inflating: chall/src/users/user.entity.ts
inflating: chall/src/users/users.module.ts
inflating: chall/src/users/users.service.ts
inflating: chall/tsconfig.build.json
inflating: chall/tsconfig.json
inflating: docker-compose.yml
/users/app.controller.ts
:
[...]
import * as safeEval from 'safe-eval';
[...]
@UseGuards(JwtAuthGuard)
@Post('exec')
executeCodeSafely(@Request() req, @Body('code') code: string) {
if (req.user.pseudo === 'admin')
try {
const result = safeEval(code);
if (!result) throw new CustomError('safeEval Failed');
return { result };
} catch (error) {
return {
from: error.from ? error.from(AppController) : 'Unknown error source',
msg: error.message,
};
}
return {
result: "You're not admin !",
};
}
[...]
In this /exec
POST route, when the user is admin
, it uses the safeEval()
to evaluate arbitrary JavaScript code.
According to safe-eval npm page, it says:
safe-eval
lets you execute JavaScript code without having to use the much discouraged and feared uponeval()
.safe-eval
has access to all the standard APIs of the V8 JavaScript Engine. By default, it does not have access to the Node.js API, but can be given access using a conext object. It is implemented using node’s vm module.
Simulating after logged in as admin
:
Hmm… Maybe we can do sandbox bypass and gain Remote Code Execution (RCE) if we’re admin
?
But how to become admin
?
Route /auth/login
:
[...]
@Post('auth/login')
async login(@Body() payload) {
const user = await this.authService.validate(payload);
return this.authService.getToken(user);
}
[...]
/auth/auth.service.ts
:
@Injectable()
export class AuthService {
[...]
async validate(payload) {
const user = await this.usersService.findOne(payload.pseudo);
if (user && user.password === getReduceMd5(payload.password)) {
return user;
}
throw new ForbiddenException('Invalid Informations');
}
getToken(payload) {
return {
access_token: this.jwtService.sign({
pseudo: payload.pseudo,
sub: payload.id,
}),
};
}
}
/**
*
* @param input Input to hash
* @returns MD5 of input, but reduced (save some room in database)
*/
function getReduceMd5(input) {
return crypto.createHash('md5').update(input).digest('hex').slice(0, 6);
}
When a user trying to login, it’ll first check the username is correct. Then, it’ll check the password is correct by calling function getReduceMd5()
.
The getReduceMd5()
is very interesting to me, as it only generate 6 characters long MD5 hash. Which means it could be vulnerable to MD5 hash collision:
┌[siunam♥earth]-(~/ctf/PwnMe-2023-8-bits/Web/Nestapp/chall)-[2023.05.07|19:33:20(HKT)]
└> sudo docker exec -it 3c7d0e1b0eac /bin/bash
bash-4.2# mysql -uuser -p
Enter password:
[...]
mysql> use db;
[...]
mysql> SELECT * FROM users;
+--------------------------------------+--------+----------+
| id | pseudo | password |
+--------------------------------------+--------+----------+
| 8d7f612d-a67a-47dd-ac69-067e77985351 | admin | 6991d0 |
| beb32133-f30a-4071-ba0a-8872930c7fad | siunam | 1a1dc9 |
+--------------------------------------+--------+----------+
Hmm… What if we brute force admin’s password, and hopefully it’s the MD5 password hash is collided…
Then, I tried to do that locally:
#!/usr/bin/env python3
from hashlib import md5
if __name__ == '__main__':
# MD5 is 128 bits long
for i in range(0xffffffff + 1):
plainText = str(i).encode('utf-8')
afterHashed = md5(plainText).hexdigest()[:6]
print(f'[*] Trying plaintext: {plainText.decode()}, after hashed: {afterHashed}', end='\r')
if afterHashed == '6991d0':
print('[+] MD5 hash collided!\n')
print('[+] Target hash: 6991d0')
print(f'[+] Before hashed: {plainText.decode()}')
print(f'[+] After hashed: {afterHashed}')
break
┌[siunam♥earth]-(~/ctf/PwnMe-2023-8-bits/Web/Nestapp)-[2023.05.07|19:42:02(HKT)]
└> python3 md5_hash_collision.py
[...]
[+] MD5 hash collided!
[+] Target hash: 6991d0
[+] Before hashed: 10979066
[+] After hashed: 6991d0
That took me 10979066 times!
However, I asked admin about that, and they say brute forcing on the remote instance is not allowed…
Hmm… Are there anything that I can escalate to admin
?…
After fumbling around, I couldn’t find anything interesting…