From a7a6349162f63063645269695e93d11c0743f280 Mon Sep 17 00:00:00 2001
From: Xusheng Required when setting or changing a flag, or replacing a flagged file.
The flag solvers submit for points. Leave empty (or tick below) and this crackme accepts no
flag submissions at all. Setting one on a crackme that had none turns auto-validation on.
diff --git a/review/templates/reviewer/viewcrackme.html b/review/templates/reviewer/viewcrackme.html
index 7bb416a..5d63b70 100644
--- a/review/templates/reviewer/viewcrackme.html
+++ b/review/templates/reviewer/viewcrackme.html
@@ -81,6 +81,10 @@ The author did not enable auto-validation.Edit crackme: "{{ crackme.name }}" by {{ crackme.author }}
+
+
+ Auto-validation
+ Quick Rules
Players submit the flag to earn points. Only reviewers can see it.
Defaults to difficulty × 100. You may choose any whole number from 100 to 600.
diff --git a/templates/rules/crackmerules.html b/templates/rules/crackmerules.html index 77ea506..3a2f300 100644 --- a/templates/rules/crackmerules.html +++ b/templates/rules/crackmerules.html @@ -71,7 +71,7 @@Auto-validation and providing source code are separate, optional choices. Tick Auto-validation if you want solvers to submit a flag for points. It requires:
CMO{...} — no
- spaces or braces inside. Reviewers can read it, because they need it to confirm your crackme is
+ spaces or braces inside. Enter the flag you observed from the uploaded file separately so it can be checked against the submitted flag. Reviewers can read it, because they need it to confirm your crackme is
solvable; it is never shown anywhere on the site.You may separately upload a private source ZIP for reviewers, with or without auto-validation. For a keygenme, diff --git a/tests/test_solves.py b/tests/test_solves.py index dcd30bb..dc40a28 100644 --- a/tests/test_solves.py +++ b/tests/test_solves.py @@ -118,6 +118,8 @@ def _upload(client, monkeypatch, tmp_path, **extra): 'file': (BytesIO(b'challenge-binary'), 'challenge.bin'), } data.update(extra) + if data.get('auto_validation') and 'observed_flag' not in data: + data['observed_flag'] = data.get('flag', '') return client.post('/upload/crackme', data=data, content_type='multipart/form-data', follow_redirects=True) @@ -162,6 +164,16 @@ def test_auto_validation_requires_a_well_formed_flag_but_no_source_archive( assert stored['source_original_filename'] is None +def test_upload_rejects_a_file_flag_that_disagrees_with_the_submitted_flag( + alice_client, db, alice, tmp_path, monkeypatch): + response = _upload(alice_client, monkeypatch, tmp_path, + auto_validation='on', flag=FLAG, + observed_flag='CTF{wrong_format}') + + assert b'flag observed from the uploaded file must match' in response.data + assert db.crackme.count_documents({'name': 'Flagged Challenge'}) == 0 + + def test_source_archive_without_auto_validation( alice_client, db, alice, tmp_path, monkeypatch): response = _upload(alice_client, monkeypatch, tmp_path, @@ -519,12 +531,15 @@ def _approval_data(crackme, **overrides): 'arch': crackme['arch'], 'platform': crackme['platform'], 'flag': crackme.get('flag') or '', + 'observed_flag': crackme.get('flag') or '', 'official_points': str(round( (crackme.get('official_difficulty') or 3) * 100 )), 'labels': crackme.get('labels', []), } data.update(overrides) + if 'flag' in overrides and 'observed_flag' not in overrides: + data['observed_flag'] = overrides['flag'] return data @@ -574,6 +589,24 @@ def test_approval_records_the_official_difficulty( assert stored['flag'] == 'CMO{reviewer_corrected}' +def test_approval_rejects_a_mismatched_observed_file_flag( + reviewer_client, db, flagged_crackme, monkeypatch, tmp_path): + from review import routes + + pending, _, _ = _review_dirs(monkeypatch, tmp_path) + db.crackme.update_one({'_id': flagged_crackme['_id']}, + {'$set': {'visible': False}}) + (pending / 'crackme' / flagged_crackme['hexid']).write_bytes(b'binary') + response = reviewer_client.post( + '/review/approvecrackme', + data=_approval_data(flagged_crackme, observed_flag='CTF{different}'), + follow_redirects=True, + ) + + assert b'flag observed from the uploaded file must match' in response.data + assert db.crackme.find_one({'_id': flagged_crackme['_id']})['visible'] is False + + @pytest.mark.parametrize('points', ['99', '601', '250.5', 'many']) def test_review_approval_rejects_invalid_points( reviewer_client, db, flagged_crackme, monkeypatch, tmp_path, points): @@ -795,9 +828,12 @@ def _edit(client, crackme, **overrides): 'arch': crackme.get('arch', ''), 'platform': crackme.get('platform', ''), 'flag': crackme.get('flag') or '', + 'observed_flag': crackme.get('flag') or '', 'official_points': str(round((crackme.get('official_difficulty') or 0) * 100) or ''), } data.update(overrides) + if 'flag' in overrides and 'observed_flag' not in overrides: + data['observed_flag'] = overrides['flag'] return client.post('/review/editcrackme', data=data, content_type='multipart/form-data') @@ -879,6 +915,15 @@ def test_admin_edit_rejects_a_malformed_flag_or_difficulty( assert stored['official_difficulty'] == 3 +def test_admin_cannot_change_flag_without_matching_file_observation( + admin_client, db, flagged_crackme): + response = _edit(admin_client, flagged_crackme, + flag='CMO{corrected}', observed_flag='CTF{different}') + + assert b'flag observed from the crackme file must match' in response.data + assert db.crackme.find_one({'_id': flagged_crackme['_id']})['flag'] == FLAG + + def test_admin_replaces_the_private_source_archive( admin_client, db, flagged_crackme, monkeypatch, tmp_path): from review import routes