Challenge dapat di akses melalui https://xss-game.appspot.com/
Level 1
Mission Description : This level demonstrates a common cause of cross-site scripting where user input is directly included in the page without proper escaping.
Interact with the vulnerable application window below and find a way to make it execute JavaScript of your choosing. You can take actions inside the vulnerable window or directly edit its URL bar.
Mission Objective : Inject a script to pop up a JavaScript alert()
in the frame below.
Once you show the alert you will be able to advance to the next level.
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 |
page_header = """ <!doctype html> <html> <head> <!-- Internal game scripts/styles, mostly boring stuff --> <script src="/static/game-frame.js"></script> <link rel="stylesheet" href="/static/game-frame-styles.css" /> </head> <body id="level1"> <img src="/static/logos/level1.png"> <div> """ page_footer = """ </div> </body> </html> """ main_page_markup = """ <form action="" method="GET"> <input id="query" name="query" value="Enter query here..." onfocus="this.value=''"> <input id="button" type="submit" value="Search"> </form> """ class MainPage(webapp.RequestHandler): def render_string(self, s): self.response.out.write(s) def get(self): # Disable the reflected XSS filter for demonstration purposes self.response.headers.add_header("X-XSS-Protection", "0") if not self.request.get('query'): # Show main search page self.render_string(page_header + main_page_markup + page_footer) else: query = self.request.get('query', '[empty]') # Our search engine broke, we found no results :-( message = "Sorry, no results were found for <b>" + query + "</b>." message += " <a href='?'>Try again</a>." # Display the results page self.render_string(page_header + message + page_footer) return application = webapp.WSGIApplication([ ('.*', MainPage), ], debug=False) |
Hints :
- To see the source of the application you can right-click on the frame and choose View Frame Source from the context menu or use your browser’s developer tools to inspect network traffic.
- What happens when you enter a presentational tag such as <h1>?
- Alright, one last hint: <script> … alert …
Solution :
Pertama masukkan sebuah kata, apa saja ke dalam searh bar. Penulis akan memasukkan kata “test”. Ini hasilnya :
Setelah input sebuah kata “test” kata yang kita cari tersebut ditampilkan kembali di kalimat error “Sorry, no results were found for (input). Try again. Ini merupakan tanda tanda pertama untuk mengetahui jika halaman tersebut dapat di XSS atau tidak. Maka kita lanjutkan dengan test kedua. Kita akan mencoba memasukkan <strike>test2</strike>
Nampaknya apapun yang kita masukan kedalam search bar akan di tampilkan ulang di page tanpa filtering apa apa sehingga dengan mencari <script>alert(1)</script>, challenge dapat diselesaikan
Level 2
Mission Description : Web applications often keep user data in server-side and, increasingly, client-side databases and later display it to users. No matter where such user-controlled data comes from, it should be handled carefully.
This level shows how easily XSS bugs can be introduced in complex apps.
Mission Objective : Inject a script to pop up an alert()
in the context of the application.
Note: the application saves your posts so if you sneak in code to execute the alert, this level will be solved every time you reload it.
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 |
<!doctype html> <html> <head> <!-- Internal game scripts/styles, mostly boring stuff --> <script src="/static/game-frame.js"></script> <link rel="stylesheet" href="/static/game-frame-styles.css" /> <!-- This is our database of messages --> <script src="/static/post-store.js"></script> <script> var defaultMessage = "Welcome!<br><br>This is your <i>personal</i>" + " stream. You can post anything you want here, especially " + "<span style='color: #f00ba7'>madness</span>."; var DB = new PostDB(defaultMessage); function displayPosts() { var containerEl = document.getElementById("post-container"); containerEl.innerHTML = ""; var posts = DB.getPosts(); for (var i=0; i<posts.length; i++) { var html = '<table class="message"> <tr> <td valign=top> ' + '<img src="/static/level2_icon.png"> </td> <td valign=top ' + ' class="message-container"> <div class="shim"></div>'; html += '<b>You</b>'; html += '<span class="date">' + new Date(posts[i].date) + '</span>'; html += "<blockquote>" + posts[i].message + "</blockquote"; html += "</td></tr></table>" containerEl.innerHTML += html; } } window.onload = function() { document.getElementById('clear-form').onsubmit = function() { DB.clear(function() { displayPosts() }); return false; } document.getElementById('post-form').onsubmit = function() { var message = document.getElementById('post-content').value; DB.save(message, function() { displayPosts() } ); document.getElementById('post-content').value = ""; return false; } displayPosts(); } </script> </head> <body id="level2"> <div id="header"> <img src="/static/logos/level2.png" /> <div>Chatter from across the Web.</div> <form action="?" id="clear-form"> <input class="clear" type="submit" value="Clear all posts"> </form> </div> <div id="post-container"></div> <table class="message"> <tr> <td valign="top"> <img src="/static/level2_icon.png"> </td> <td class="message-container"> <div class="shim"></div> <form action="?" id="post-form"> <textarea id="post-content" name="content" rows="2" cols="50"></textarea> <input class="share" type="submit" value="Share status!"> <input type="hidden" name="action" value="sign"> </form> </td> </tr> </table> </body> </html> |
1 2 3 4 5 6 7 8 9 10 |
class MainPage(webapp.RequestHandler): def render_template(self, filename, context={}): path = os.path.join(os.path.dirname(__file__), filename) self.response.out.write(template.render(path, context)) def get(self): self.render_template('index.html') application = webapp.WSGIApplication([ ('.*', MainPage) ], debug=False) |
1 2 3 4 5 6 7 8 9 10 |
class MainPage(webapp.RequestHandler): def render_template(self, filename, context={}): path = os.path.join(os.path.dirname(__file__), filename) self.response.out.write(template.render(path, context)) def get(self): self.render_template('index.html') application = webapp.WSGIApplication([ ('.*', MainPage) ], debug=False) |
Hints :
- Note that the “welcome” post contains HTML, which indicates that the template doesn’t escape the contents of status messages.
- Entering a <script> tag on this level will not work. Try an element with a JavaScript attribute instead.
- This level is sponsored by the letters i, m and g and the attribute
onerror.
Solution :
Pertama penulis mencoba memasukkan <script>alert(1)</script> dan melakukan share status, namun tidak terjadi apa apa. Nampaknya untuk level ini dibutuhkan cara lain untuk melakukan XSS. Namun <strong>test</strong> masih dapat bekerja yang berarti kita masih bisa melakukan XSS namun dengan cara lain selain menggunakan <script> tag. Untuk menyelesaikan challenge ini dapat menggunakan <img src=’1′ onerror=’alert(1)’ >
Level 3
Mission Description : As you’ve seen in the previous level, some common JS functions are execution sinks which means that they will cause the browser to execute any scripts that appear in their input. Sometimes this fact is hidden by higher-level APIs which use one of these functions under the hood.
The application on this level is using one such hidden sink.
Mission Objective : As before, inject a script to pop up a JavaScript alert()
in the app.
Since you can’t enter your payload anywhere in the application, you will have to manually edit the address in the URL bar below.
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 |
<!doctype html> <html> <head> <!-- Internal game scripts/styles, mostly boring stuff --> <script src="/static/game-frame.js"></script> <link rel="stylesheet" href="/static/game-frame-styles.css" /> <!-- Load jQuery --> <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"> </script> <script> function chooseTab(num) { // Dynamically load the appropriate image. var html = "Image " + parseInt(num) + "<br>"; html += "<img src='/static/level3/cloud" + num + ".jpg' />"; $('#tabContent').html(html); window.location.hash = num; // Select the current tab var tabs = document.querySelectorAll('.tab'); for (var i = 0; i < tabs.length; i++) { if (tabs[i].id == "tab" + parseInt(num)) { tabs[i].className = "tab active"; } else { tabs[i].className = "tab"; } } // Tell parent we've changed the tab top.postMessage(self.location.toString(), "*"); } window.onload = function() { chooseTab(unescape(self.location.hash.substr(1)) || "1"); } // Extra code so that we can communicate with the parent page window.addEventListener("message", function(event){ if (event.source == parent) { chooseTab(unescape(self.location.hash.substr(1))); } }, false); </script> </head> <body id="level3"> <div id="header"> <img id="logo" src="/static/logos/level3.png"> <span>Take a tour of our cloud data center.</a> </div> <div class="tab" id="tab1" onclick="chooseTab('1')">Image 1</div> <div class="tab" id="tab2" onclick="chooseTab('2')">Image 2</div> <div class="tab" id="tab3" onclick="chooseTab('3')">Image 3</div> <div id="tabContent"> </div> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 |
class MainPage(webapp.RequestHandler): def render_template(self, filename, context={}): path = os.path.join(os.path.dirname(__file__), filename) self.response.out.write(template.render(path, context)) def get(self): self.render_template('index.html') application = webapp.WSGIApplication([ ('.*', MainPage), ], debug=False) |
Hints :
- To locate the cause of the bug, review the JavaScript to see where it handles user-supplied input.
- Data in the
window.location
object can be influenced by an attacker. - When you’ve identified the injection point, think about what you need to do to sneak in a new HTML element.
- As before, using
<script> ...
as a payload won’t work because the browser won’t execute scripts added after the page has loaded.
Solution : Pertama penulis mencoba mengecek 1 per 1 image dari cloud data center yang disediakan, terdapat 3 image dan setiap image di klik akan mengubah URL nya sesuai nomor imagenya.
https://xss-game.appspot.com/level3/frame#1
https://xss-game.appspot.com/level3/frame#2
https://xss-game.appspot.com/level3/frame#3
Nampaknya, kita bisa memasukkan input kita dibelakang pound symbol di url. Namun pada challenge ini, kita tidak dapat menggunakan <script> tag juga karena browsernya tidak akan mengeksekusi script setelah pagenya sudah selesai loading. Sehingga mari kita coba payload yang sama seperti sebelumnya, namun kita letakkan di belakang pound symbol di url. Challenge dapat diselesaikan dengan
https://xss-game.appspot.com/level3/frame#<img src=’1′ onerror=alert(1) >
Level 4
Mission Description : Every bit of user-supplied data must be correctly escaped for the context of the page in which it will appear. This level shows why.
Mission Objective : Inject a script to pop up a JavaScript alert()
in the application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!doctype html> <html> <head> <!-- Internal game scripts/styles, mostly boring stuff --> <script src="/static/game-frame.js"></script> <link rel="stylesheet" href="/static/game-frame-styles.css" /> </head> <body id="level4"> <img src="/static/logos/level4.png" /> <br> <form action="" method="GET"> <input id="timer" name="timer" value="3"> <input id="button" type="submit" value="Create timer"> </form> </form> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class MainPage(webapp.RequestHandler): def render_template(self, filename, context={}): path = os.path.join(os.path.dirname(__file__), filename) self.response.out.write(template.render(path, context)) def get(self): # Disable the reflected XSS filter for demonstration purposes self.response.headers.add_header("X-XSS-Protection", "0") if not self.request.get('timer'): # Show main timer page self.render_template('index.html') else: # Show the results page timer= self.request.get('timer', 0) self.render_template('timer.html', { 'timer' : timer }) return application = webapp.WSGIApplication([ ('.*', MainPage), ], debug=False) |
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 |
<!doctype html> <html> <head> <!-- Internal game scripts/styles, mostly boring stuff --> <script src="/static/game-frame.js"></script> <link rel="stylesheet" href="/static/game-frame-styles.css" /> <script> function startTimer(seconds) { seconds = parseInt(seconds) || 3; setTimeout(function() { window.confirm("Time is up!"); window.history.back(); }, seconds * 1000); } </script> </head> <body id="level4"> <img src="/static/logos/level4.png" /> <br> <img src="/static/loading.gif" onload="startTimer('{{ timer }}');" /> <br> <div id="message">Your timer will execute in {{ timer }} seconds.</div> </body> </html> |
Hints :
- Take a look at how the
startTimer
function is called. - When browsers parse tag attributes, they HTML-decode their values first. <foo bar=’z’> is the same as <foo bar=’z’
- Try entering a single quote (‘) and watch the error console.
Solution :
Pertama penulis menekan tombol create timer dan melihat apa yang terjadi. Saat tombol ditekan, url akan berubah menjadi
https://xss-game.appspot.com/level4/frame?timer=3
setelah 3 detik, alert ini akan muncul dan url kembali menjadi semula lagi. Penulis mengecek hint untuk bantuan, pada hint nomor 3, kita diminta untuk memasukkan ‘ kedalam timer. Setelah tombol create timer ditekan, tidak terjadi apa apa setelah begitu lama.
Pada console, terdapat sebuah error
Uncaught SyntaxError: missing ) after argument list
Penulis menyadari cara yang benar untuk melakukan xss, hal pertama yang dilakukan adalah melihat source code yang bekerja saat input di submit.
Challenge ini dapat diselesaikan dengan mensubmit timer dengan
3′); alert(‘1
Mengapa XSS dapat terjadi ketika kita memasukkan itu? Hal ini karena yang kita input akan langsung masuk ke fungsi yang dijalankan onload tanpa dilakukan filterin terlebih dahulu. Sehingga kurang lebih akan menjadi
<img src=”static/loading.gif” onload=”startTimer(‘3’);alert(‘1’);” />
Tidak terjadi error pada syntax, dan XSS sukses dilakukan
Level 5
Mission Description : Cross-site scripting isn’t just about correctly escaping data. Sometimes, attackers can do bad things even without injecting new elements into the DOM.
Mission Objective : Inject a script to pop up an alert()
in the context of the application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!doctype html> <html> <head> <!-- Internal game scripts/styles, mostly boring stuff --> <script src="/static/game-frame.js"></script> <link rel="stylesheet" href="/static/game-frame-styles.css" /> </head> <body id="level5"> <img src="/static/logos/level5.png" /><br><br> Thanks for signing up, you will be redirected soon... <script> setTimeout(function() { window.location = '{{ next }}'; }, 5000); </script> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class MainPage(webapp.RequestHandler): def render_template(self, filename, context={}): path = os.path.join(os.path.dirname(__file__), filename) self.response.out.write(template.render(path, context)) def get(self): # Disable the reflected XSS filter for demonstration purposes self.response.headers.add_header("X-XSS-Protection", "0") # Route the request to the appropriate template if "signup" in self.request.path: self.render_template('signup.html', {'next': self.request.get('next')}) elif "confirm" in self.request.path: self.render_template('confirm.html', {'next': self.request.get('next', 'welcome')}) else: self.render_template('welcome.html', {}) return application = webapp.WSGIApplication([ ('.*', MainPage), ], debug=False) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!doctype html> <html> <head> <!-- Internal game scripts/styles, mostly boring stuff --> <script src="/static/game-frame.js"></script> <link rel="stylesheet" href="/static/game-frame-styles.css" /> </head> <body id="level5"> <img src="/static/logos/level5.png" /><br><br> <!-- We're ignoring the email, but the poor user will never know! --> Enter email: <input id="reader-email" name="email" value=""> <br><br> <a href="{{ next }}">Next >></a> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!doctype html> <html> <head> <!-- Internal game scripts/styles, mostly boring stuff --> <script src="/static/game-frame.js"></script> <link rel="stylesheet" href="/static/game-frame-styles.css" /> </head> <body id="level5"> Welcome! Today we are announcing the much anticipated<br><br> <img src="/static/logos/level5.png" /><br><br> <a href="/level5/frame/signup?next=confirm">Sign up</a> for an exclusive Beta. </body> </html> |
- The title of this level is a hint.
- It is useful look at the source of the signup frame and see how the URL parameter is used.
- If you want to make clicking a link execute Javascript (without using the
onclick
handler), how can you do it? - If you’re really stuck, take a look at this IETF draft
Solution : Penulis mengklik Sign up dan menemukan sebuah box email dan tombol hyperlink untuk Next >> . Pada signup.html terdapat sebuah comment
<!– We’re ignoring the email, but the poor user will never know! –>
Dari sini dapat disimpulkan bahwa kita tidak akan menggunakan field input email untuk melakukan XSS.
Kemudian pada line 15 di signup.html terdapat
<a href=”{{ next }}”>Next >></a>
yang berarti inputan yang bernama next akan dimasukkan dan menggantikan posisi {{ next }} . Penulis baru menyadari ternyata pada url terjadi perubahan menjadi
https://xss-game.appspot.com/level5/frame/signup?next=confirm
terdapat http request dengan metode GET, next=confirm sehingga akan membuat line tadi menjadi seperti
<a href=”confirm”>Next >></a>
Mari kita ubah sedikit untuk melakukan XSS dengan referensi dari hints nomor 4, menjadi
<a href=”javascript:alert(1)”>Next >></a>
Berhasil
Level 6
Mission Description : Complex web applications sometimes have the capability to dynamically load JavaScript libraries based on the value of their URL parameters or part of location.hash
.
This is very tricky to get right — allowing user input to influence the URL when loading scripts or other potentially dangerous types of data such asXMLHttpRequest
often leads to serious vulnerabilities.
Mission Objective : Find a way to make the application request an external file which will cause it to execute an alert()
.
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 |
<!doctype html> <html> <head> <!-- Internal game scripts/styles, mostly boring stuff --> <script src="/static/game-frame.js"></script> <link rel="stylesheet" href="/static/game-frame-styles.css" /> <script> function setInnerText(element, value) { if (element.innerText) { element.innerText = value; } else { element.textContent = value; } } function includeGadget(url) { var scriptEl = document.createElement('script'); // This will totally prevent us from loading evil URLs! if (url.match(/^https?:\/\//)) { setInnerText(document.getElementById("log"), "Sorry, cannot load a URL containing \"http\"."); return; } // Load this awesome gadget scriptEl.src = url; // Show log messages scriptEl.onload = function() { setInnerText(document.getElementById("log"), "Loaded gadget from " + url); } scriptEl.onerror = function() { setInnerText(document.getElementById("log"), "Couldn't load gadget from " + url); } document.head.appendChild(scriptEl); } // Take the value after # and use it as the gadget filename. function getGadgetName() { return window.location.hash.substr(1) || "/static/gadget.js"; } includeGadget(getGadgetName()); // Extra code so that we can communicate with the parent page window.addEventListener("message", function(event){ if (event.source == parent) { includeGadget(getGadgetName()); } }, false); </script> </head> <body id="level6"> <img src="/static/logos/level6.png"> <img id="cube" src="/static/level6_cube.png"> <div id="log">Loading gadget...</div> </body> </html> |
1 2 3 4 5 6 7 8 9 |
class MainPage(webapp.RequestHandler): def render_template(self, filename, context={}): path = os.path.join(os.path.dirname(__file__), filename) self.response.out.write(template.render(path, context)) def get(self): self.render_template('index.html') application = webapp.WSGIApplication([ ('.*', MainPage), ], debug=False) |
Hints :
- See how the value of the location fragment (after
#
) influences the URL of the loaded script. - Is the security check on the gadget URL really foolproof?
- Sometimes when I’m frustrated, I feel like screaming…
- If you can’t easily host your own evil JS file, see if google.com/jsapi?callback=foo will help you here.
Solution :
Tidak ada field input dimanapun, sehingga penulis langsung saja melihat source code dan hints yang ada dan mengambil kesimpulan bahwa yang perlu dilakukan adalah redirection XSS dengan cara menghost sebuah javascript file di sebuah website.
Terdapat beberapa bagian yang perlu diperhatikan dalam source code yang diberikan yang berhubungan dengan URL pada saat itu
https://xss-game.appspot.com/level6/frame#/static/gadget.js
-
12function includeGadget(url) {var scriptEl = document.createElement('script');
-
12345if (url.match(/^https?:\/\//)) {setInnerText(document.getElementById("log"),"Sorry, cannot load a URL containing \"http\".");return;}
-
1234// Take the value after # and use it as the gadget filename.function getGadgetName() {return window.location.hash.substr(1) || "/static/gadget.js";}
-
1includeGadget(getGadgetName());
-
123456// Extra code so that we can communicate with the parent pagewindow.addEventListener("message", function(event){if (event.source == parent) {includeGadget(getGadgetName());}}, false);
Kita akan melakukan input URL setelah # untuk melakukan sebuah file inclusion dari website yang kita buat dan berisi file javascript untuk melakukan alert(1), namun tidak lupa juga terdapat sebuah filter yang melarang penggunaan http, namun pengecekan ini sangat mudah untuk dilewatkan dengan menggunakan capital contohnya Http atau HTTP karena pengecekannya case-insensitive. Penulis sempat stuck di titik ini dan menentukan untuk mencoba teknik lain, yaitu Data URL.
Referensi dapat dicek dari https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
Challenge dapat diselesaikan dengan mengubah URL menjadi
https://xss-game.appspot.com/level6/frame#data:text/javascript,alert(1)
Challenge Complete !