FROM base as production ENV NODE_ENV=production ENV TARGET_HOST=private:5000 RUN npm install -g nodemon && npm install RUN npm ci COPY . / CMD ["node", "bin/www"]
FROM base as dev ENV NODE_ENV=development ENV DEBUG=frontend:* ENV TARGET_HOST=private:5000 RUN npm install -g nodemon && npm install COPY . / CMD ["nodemon", "bin/www"]
FROM base as local ENV NODE_ENV=development ENV DEBUG=frontend:* ENV TARGET_HOST=localhost:5050 RUN npm install -g nodemon && npm install COPY . / CMD ["nodemon", "bin/www"]
When I check a docker file, I can know to was open a 5050/5000 port. Viz, I can able use a localhost:5050, localhost:5000.
1 2 3 4 5 6 7 8 9 10 11 12 13
defRunRollbackDB(dbhash): try: if os.environ['ENV'] == 'LOCAL': return if dbhash isNone: return"dbhash is None" dbhash = ''.join(e for e in dbhash if e.isalnum()) if os.path.isfile('backup/'+dbhash): withopen('FLAG', 'r') as f: flag = f.read() return flag else: return"Where is file?"
And first, when I see an important flag reading condition, If environment of server is not Local and exist a file called 'backup/' + dbhash, retrun a flag.
1 2 3 4 5 6 7 8 9 10 11 12
@app.route('/coin', methods=['GET']) defcoin(): try: response = app.response_class() language = LanguageNomarize(request) response.headers["Lang"] = language data = getCoinInfo() response.data = json.dumps(data) return response except Exception as e : err = 'Error On {f} : {c}, Message, {m}, Error on line {l}'.format(f = sys._getframe().f_code.co_name ,c = type(e).__name__, m = str(e), l = sys.exc_info()[-1].tb_lineno) logger.error(err)
First, I can see that when api server send a request to /coin , put a result value of LanguageNomarize() function to header called Lang and to reponse after bring an information of coin using a getCoinInfo() function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
defLanguageNomarize(request): if request.headers.get('Lang') isNone: return"en" else: regex = '^[!@#$\\/.].*/.*'# Easy~~ language = request.headers.get('Lang') language = re.sub(r'%00|%0d|%0a|[!@#$^]|\.\./', '', language) if re.search(regex,language): return request.headers.get('Lang') try: data = requests.get(request.host_url+language, headers=request.headers) if data.status_code == 200: return data.text else: return request.headers.get('Lang') except: return request.headers.get('Lang')
When I see a LanguateNomarize() function, If to exist a header called Lang, confirm a value of header using regular expression, and I can see to send a request using a request.get() function. In here, Occur an ssrf vulnerability because I can send a request of where I want after modifying a request.host_url and language.
1 2 3 4 5
@app.route('/integrityStatus', methods=['GET']) defintegritycheck(): data = {'db':'database/master.db','dbhash':activity.dbHash} data = json.dumps(data) return data
Second, I’ll check the /integrityStatus. We usually can’t send a request because /integrityStatus not communicating to api server. But I can find a value of dbhash by making a request to /integrityStatus using an ssrf vulnerability.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
@app.route('/rollback', methods=['GET']) defrollback(): try: if request.headers.get('Sign') == None: return json.dumps(status['sign']) else: if SignCheck(request): pass else: return json.dumps(status['sign'])
if request.headers.get('Key') == None: return json.dumps(status['key']) result = activity.IntegrityCheck(request.headers.get('Key'),request.args.get('dbhash')) return result except Exception as e : err = 'Error On {f} : {c}, Message, {m}, Error on line {l}'.format(f = sys._getframe().f_code.co_name ,c = type(e).__name__, m = str(e), l = sys.exc_info()[-1].tb_lineno) logger.error(err) return json.dumps(status['error']), 404
Third, I can see that when I see a /rollback, If value of SignCheck() is true and exist a value of header called Key, call an IntegrityCheck() method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
defIntegrityCheck(self,key, dbHash):
if self.integrityKey == key: pass else: return json.dumps(status['key']) if self.dbHash != dbHash: flag = RunRollbackDB(dbHash) logger.debug('DB File changed!!'+dbHash) file = open(os.environ['DBFILE'],'rb').read() self.dbHash = hashlib.md5(file).hexdigest() self.integrityKey = hashlib.sha512((self.dbHash).encode('ascii')).hexdigest() return flag return"DB is safe!"
When I see the IntegrityCheck() method, the value of self.integrityKey and the value of Key are the same, and if the value of self.dbHash and dbHash are not the same, I can see that the RunRollbackDB() function is executed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
defRunRollbackDB(dbhash): try: if os.environ['ENV'] == 'LOCAL': return if dbhash isNone: return"dbhash is None" dbhash = ''.join(e for e in dbhash if e.isalnum()) if os.path.isfile('backup/'+dbhash): withopen('FLAG', 'r') as f: flag = f.read() return flag else: return"Where is file?" except Exception as e : logger.error('Error On {f} : {c}, Message, {m}, Error on line {l}'.format(f = sys._getframe().f_code.co_name ,c = type(e).__name__, m = str(e), l = sys.exc_info()[-1].tb_lineno)) return"exception!!" pass
RunRollbackDB() is a function that reads and returns a FLAG file if the backup/dbhash file exists, as seen above.
So I thought with the following scenario at first
Scenario 1
Using ssrf vulnerability to send a request to /integrityStatus to get the value of dbHash.
Read the FLAG by sending a request to /rollback using the retrieved value of dbHash.
FLAG is returned as a header value of lang.
However, an exploit was attempted using the above scenario, but the attack could not be performed because the value of self.dbHash and dbHash in the IntegrityCheck() function had to be different.
1 2 3 4 5 6 7
defWriteFile(url): local_filename = url.split('/')[-1] with requests.get(url, stream=True) as r: r.raise_for_status() withopen('backup/'+local_filename, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk)
So, when I analyzed the code again, I could see that the WriteFile() function was used to create a file under backup/. So, if you create any file using WriteFile function and send the name of the file to dbHash, it is not the same as the value of self.dbHash, which is a variable in the class, and it is Because it exists, it is enough to bypass it and read the flags.
When I see inside the WriteFile() function, a request is sent using the requests.get() function, and the value of url.split('/')[-1] is used as the file name to create it.
The WriteFile() function is called from /download. At this time, if the header value Sign exists, the return value of the SignCheck() function is true, and the src parameter value exists, WriteFile() function is executed.
Scenario 2
Using the ssrf vulnerability, a request was sent to /download to create a random file.
Again, using the ssrf vulnerability, sending a request to /rollback to read the FLAG
In this case, the self.dbHash != dbHash syntax is bypassed by using the file name.
Finally, just read the Lang header.
A note of caution
When creating a file, a request is sent using the requests.get() function, so when creating a file, a URL must be sent to create it.
The value of dbHash is parsed once more within the RunRollbackDB() function, and the value of e.isalnum() must be true.
When reading the flag, the value of the environment variable ENV should not be local, so port 5000 should be used instead of port 5050.