Tags: flask web python php
Rating:
# Google CTF 2023 — Under-Construction Writeup
When you start any challenge at the Google CTF 2023, the first things you will usually look at are the challenge description, attachments and in case of a web challenge the web pages.
![](https://cdn-images-1.medium.com/max/800/1*EJ3YRPMvQ2i2HQcOuARAkA.png)
Curiously, Under-Construction contains two web pages, one written in Python, the other on in PHP. That might come in handy!
### Getting an overview
Now that we know what we can work with, let’s look at everything we got. Starting with the web pages.
![](https://cdn-images-1.medium.com/max/800/1*dzj197VjYKRFx3RorIrcog.png)
![](https://cdn-images-1.medium.com/max/800/1*Ifxz2FYKVTqPkLlu78k7eg.png)
![](https://cdn-images-1.medium.com/max/800/1*TaZvtOtnAUApqzaAPJdkLg.png)
![](https://cdn-images-1.medium.com/max/800/1*pZRCi-mkdlR2_32owIOMVA.png)
The attached archive contains the source of the challenge. So did all challenges in this years Google CTF (which I prefer to having to tap in the dark).
![](https://cdn-images-1.medium.com/max/800/1*MlQLus9_u0La8oRcw2adyg.png)
### So what does the application do
The premise for Under-Construction is that a company developed a Python Flask application in the past. The application is relatively simple. You can register accounts and log in with them. When registering, you can select one of the four tiers “BLUE”, “RED”, “GREEN” and “GOLD”. The tiers don’t actually do anything on the Python side. The only difference is that users can’t register “GOLD” accounts.
Some time after that, management decided to migrate from Python to PHP however. So now the company is in the process of building a separate PHP application and facing out the Python application. To get the flag in Under-Construction, you have to log in on the PHP site with a “GOLD” account. Once you do that, PHP will echo back the flag to you.
### Searching for an exploit
Looking through the code, we find two sections of particular interest. Those being the Python and PHP registration logic. Registration is only possible in the Python application. Whenever a user registers there, however, a call is made to the PHP application to also register the user there.
php/account\_migrator.php
```php
exec("CREATE TABLE IF NOT EXISTS Users (username varchar(15) NOT NULL, password\_hash varchar(60) NOT NULL, tier varchar(10) NOT NULL, PRIMARY KEY (username));");
$stmt = $pdo->prepare("INSERT INTO Users Values(?,?,?);");
$stmt->execute([$username, $hash, $tier]);
echo "User inserted";
} catch (PDOException $e) {
throw new PDOException(
message: $e->getMessage(),
code: (int) $e->getCode()
);
}
}
?>
```
```python
flask/authorized_routes.py
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# [...] Boilerplate code above not displayed
@authorized.route('signup', methods=['POST'])
def signup_post():
raw_request = request.get_data()
username = request.form.get('username')
password = request.form.get('password')
tier = models.Tier(request.form.get('tier'))
if(tier == models.Tier.GOLD):
flash('GOLD tier only allowed for the CEO')
return redirect(url_for('authorized.signup'))
if(len(username) > 15 or len(username) < 4):
flash('Username length must be between 4 and 15')
return redirect(url_for('authorized.signup'))
user = models.User.query.filter_by(username=username).first()
if user:
flash('Username address already exists')
return redirect(url_for('authorized.signup'))
new_user = models.User(username=username,
password=generate_password_hash(password, method='sha256'), tier=tier.name)
db.session.add(new_user)
db.session.commit()
requests.post(f"http://{PHP_HOST}:1337/account_migrator.php",
headers={"token": TOKEN, "content-type": request.headers.get("content-type")}, data=raw_request)
return redirect(url_for('authorized.login'))
```
When a user registers using the Python endpoint, after checks are made and registration is done, the request is forwarded to the PHP `account_migrator.php` endpoint. One thing of note here is that checks for whether a user registers as “GOLD” or not only happen in Python. The PHP endpoint does however check for the `MIGRATOR_TOKEN` before registering a new user. Otherwise we could directly address the endpoint and register a “GOLD” user that way.
### The exploit
To register a “GOLD” user, we can exploit a difference in parameter parsing behavior between Python and PHP and the fact that the Python endpoint forwards the raw request to PHP instead of the values it reads from the request.
```python
def signup_post():
raw_request = request.get_data()
username = request.form.get('username')
password = request.form.get('password')
tier = models.Tier(request.form.get('tier'))
# run checks and insert new user into database
requests.post(f"http://{PHP_HOST}:1337/account_migrator.php",
headers={"token": TOKEN, "content-type": request.headers.get("content-type")}, data=raw_request) # data=raw_request, this is where the exploit lies
```
Say a request contained a POST parameter multiple times: `curl -X POST [https://example.com](https://example.com) -d "a=foobar&b=foo&b=bar"`. Would we get “foo” or “bar” for the value of `b`? The answer is that it depends on the programming language and framework. Python Flask takes the first occurrence, so `b` would have the value “foo”. PHP however takes the last value, so `b` would have the value “bar”. We can exploit that!
Crafting a registration request containing an allowed tier first and the “GOLD” tier second for the tier parameter will create a “GOLD” user for us.
curl -X POST https://under-construction-web.2023.ctfcompetition.com/signup -d "username=kukhofhacker&password=pwnd&tier=blue&tier=gold"
We can now log into the PHP application using the credentials we specified (username “kukhofhacker” and “pwnd” as password). Once we do that, we got our flag!
![](https://cdn-images-1.medium.com/max/800/1*S8mUQEmUrXRY1eFV3a5C0w.png)
![](https://cdn-images-1.medium.com/max/800/1*H40sYuaHeyekCPXffKTiKQ.png)
By [Jonas Heschl](https://medium.com/@jonas.heschl) on [June 26, 2023](https://medium.com/p/8a3d24f71bb5).
[Canonical link](https://medium.com/@jonas.heschl/google-ctf-2023-under-construction-writeup-8a3d24f71bb5)