Dockerflag
Plongeons dans le magnifique outil de gestion de code : Git...
Description du challenge
- Nom du CTF : 404CTF 2025
- Catégorie : Forensic
- Difficulté : Facile
- Date : Mai 2025
On nous donne le challenge suivant :
Découverte de l'archive
Quand on fait une première extraction de l'archive, on remarque qu'il y a un ensemble de sous-archives à extraire.
Autant gagner du temps et rédiger un court script bash pour extraire l'ensembles des archives récursivement :
#!/bin/bash
find . -type f -name "*.tar.gz" | while read archive; do
dir=$(dirname "$archive")
echo "Extracting: $archive"
tar -xzf "$archive" -C "$dir"
if [ $? -eq 0 ]; then
echo "Successfully extracted $archive"
else
echo "Failed to extract $archive" >&2
fi
done
echo "All archives have been extracted."
Beaucoup de dossiers vides. Mais ce qui est intéressant est évidemment le dossier app
et son dossier .git
, étant donné que l'on évoque la suppression du Docker d'un Gitlab interne
.
Dans les logs, quelques commits sont notés :
Exploration des objets Git
On rédige un script Python pour extraire les nombreux objets Git et les afficher de manière lisible.
import os
import zlib
import binascii
import re
from collections import namedtuple
# Define Git object types
GitObject = namedtuple('GitObject', ['type', 'size', 'data', 'hash'])
def get_object_path(repo_path, object_hash):
"""Convert object hash to path in .git/objects"""
return os.path.join(repo_path, 'objects', object_hash[:2], object_hash[2:])
def read_object(repo_path, object_hash):
"""Read and decompress a git object"""
path = get_object_path(repo_path, object_hash)
try:
with open(path, 'rb') as f:
compressed_data = f.read()
decompressed = zlib.decompress(compressed_data)
# Find the null byte separating header from content
null_pos = decompressed.find(b'\x00')
header = decompressed[:null_pos].decode('utf-8')
content = decompressed[null_pos+1:]
# Parse header (format: "type size\x00")
object_type, size = header.split()
size = int(size)
return GitObject(object_type, size, content, object_hash)
except FileNotFoundError:
print(f"Object {object_hash} not found")
return None
except Exception as e:
print(f"Error reading object {object_hash}: {e}")
return None
def parse_tree(tree_data):
"""Parse a tree object into its entries"""
result = []
i = 0
while i < len(tree_data):
# Find the null byte that separates mode+name from SHA
null_pos = tree_data.find(b'\x00', i)
if null_pos == -1:
break
# Extract mode and name
mode_name = tree_data[i:null_pos].decode('utf-8')
space_pos = mode_name.find(' ')
mode = mode_name[:space_pos]
name = mode_name[space_pos+1:]
# Extract SHA (20 bytes)
sha_bytes = tree_data[null_pos+1:null_pos+21]
sha_hex = binascii.hexlify(sha_bytes).decode('ascii')
result.append((mode, name, sha_hex))
i = null_pos + 21
return result
def parse_commit(commit_data):
"""Parse a commit object into its components"""
commit_text = commit_data.decode('utf-8')
# Extract tree reference
tree_match = re.search(r'tree (\w+)', commit_text)
tree = tree_match.group(1) if tree_match else None
# Extract parent(s)
parents = re.findall(r'parent (\w+)', commit_text)
# Extract author info
author_match = re.search(r'author (.*?) <(.*?)> (\d+ [+-]\d+)', commit_text)
author = {
'name': author_match.group(1),
'email': author_match.group(2),
'date': author_match.group(3)
} if author_match else None
# Extract committer info
committer_match = re.search(r'committer (.*?) <(.*?)> (\d+ [+-]\d+)', commit_text)
committer = {
'name': committer_match.group(1),
'email': committer_match.group(2),
'date': committer_match.group(3)
} if committer_match else None
# Extract message
message_match = re.search(r'\n\n(.*)', commit_text, re.DOTALL)
message = message_match.group(1) if message_match else ""
return {
'tree': tree,
'parents': parents,
'author': author,
'committer': committer,
'message': message.strip()
}
def find_git_objects(repo_path):
"""Find all git objects in the repository"""
objects_dir = os.path.join(repo_path, 'objects')
objects = []
# Skip packfiles for simplicity
for dirpath, dirnames, filenames in os.walk(objects_dir):
# Skip info and pack directories
if dirpath.endswith('/info') or dirpath.endswith('/pack'):
continue
dir_name = os.path.basename(dirpath)
if len(dir_name) == 2 and all(c in '0123456789abcdef' for c in dir_name):
for filename in filenames:
if all(c in '0123456789abcdef' for c in filename):
objects.append(dir_name + filename)
return objects
def analyze_git_repo(repo_path):
"""Analyze the Git repository and print object information"""
print(f"Analyzing Git repository at: {repo_path}")
# Find refs (branches, tags)
refs_path = os.path.join(repo_path, 'refs')
heads_path = os.path.join(refs_path, 'heads')
print("\n=== Branches ===")
if os.path.exists(heads_path):
for branch in os.listdir(heads_path):
branch_path = os.path.join(heads_path, branch)
if os.path.isfile(branch_path):
with open(branch_path, 'r') as f:
commit_hash = f.read().strip()
print(f"Branch '{branch}' points to commit: {commit_hash}")
# Follow the commit chain
print(f"\nCommit history for '{branch}':")
current_hash = commit_hash
while current_hash:
commit_obj = read_object(repo_path, current_hash)
if not commit_obj or commit_obj.type != 'commit':
break
commit_data = parse_commit(commit_obj.data)
print(f"Commit: {current_hash}")
print(f"Author: {commit_data['author']['name']} <{commit_data['author']['email']}>")
print(f"Date: {commit_data['author']['date']}")
print(f"Message: {commit_data['message']}")
print(f"Tree: {commit_data['tree']}")
# Follow the tree to see files
tree_obj = read_object(repo_path, commit_data['tree'])
if tree_obj and tree_obj.type == 'tree':
print("\n Files in this commit:")
tree_entries = parse_tree(tree_obj.data)
for mode, name, obj_hash in tree_entries:
file_obj = read_object(repo_path, obj_hash)
obj_type = file_obj.type if file_obj else "unknown"
if obj_type == 'blob':
try:
content_preview = file_obj.data[:100].decode('utf-8')
if len(file_obj.data) > 100:
content_preview += "..."
except UnicodeDecodeError:
content_preview = "(binary data)"
print(f" {mode} {name} ({obj_hash}) - {obj_type}")
print(f" Content: {content_preview}")
elif obj_type == 'tree':
print(f" {mode} {name}/ ({obj_hash}) - {obj_type}")
# Move to parent commit if it exists
current_hash = commit_data['parents'][0] if commit_data['parents'] else None
print()
if __name__ == "__main__":
# Path to the .git directory
git_path = os.path.dirname(os.path.abspath(__file__))
# Run the analysis
analyze_git_repo(git_path)
On obtient alors la sortie suivante :
=== Branches ===
Branch 'main' points to commit: e3a5491ad536b35974022c3b521d3b48880afb68
Commit history for 'main':
Commit: e3a5491ad536b35974022c3b521d3b48880afb68
Author: Alba Laine <stagiare@docker.flag>
Date: 1741022248 +0000
Message: Add HTML website
Tree: 1cd55b1dad90de7013822452467b23374adf1d96
Files in this commit:
100644 app.py (822a5257be1ed6d883e84877f7ba2253b294fa96) - blob
Content: import os
from flask import Flask, render_template
from dotenv import load_dotenv
load_dotenv()
SE...
100644 requirements.txt (5586fa52c83891ac0489f6f17b6bae9236bbccd0) - blob
Content: blinker==1.9.0
click==8.1.8
Flask==3.1.0
itsdangerous==2.2.0
Jinja2==3.1.5
MarkupSafe==3.0.2
python-...
40000 static/ (2ce2d47d98a2619eb78554fef2715d963292a3a8) - tree
40000 templates/ (dbe4f9e3a6014eaf1e13f07660284fc5465a3cb4) - tree
Commit: b34c648f6790f6dee4340767ddf4b077f639132d
Author: Alba Laine <stagiare@docker.flag>
Date: 1741022248 +0000
Message: Requirements of website
Tree: eb633388d340c6d9613fe7d48e8e4b56ec4460d7
Files in this commit:
100644 app.py (822a5257be1ed6d883e84877f7ba2253b294fa96) - blob
Content: import os
from flask import Flask, render_template
from dotenv import load_dotenv
load_dotenv()
SE...
100644 requirements.txt (5586fa52c83891ac0489f6f17b6bae9236bbccd0) - blob
Content: blinker==1.9.0
click==8.1.8
Flask==3.1.0
itsdangerous==2.2.0
Jinja2==3.1.5
MarkupSafe==3.0.2
python-...
40000 static/ (2ce2d47d98a2619eb78554fef2715d963292a3a8) - tree
Commit: 514443de0db750428f03d41d2be47e8c6d066981
Author: Alba Laine <stagiare@docker.flag>
Date: 1741022248 +0000
Message: Add static ressources
Tree: e8deb9b0d6324225d8b728dece8b2de908abcd81
Files in this commit:
100644 app.py (822a5257be1ed6d883e84877f7ba2253b294fa96) - blob
Content: import os
from flask import Flask, render_template
from dotenv import load_dotenv
load_dotenv()
SE...
40000 static/ (2ce2d47d98a2619eb78554fef2715d963292a3a8) - tree
Commit: 3d0717cb911d00b3e5033ba8c0c83df069e3e144
Author: Alba Laine <stagiare@docker.flag>
Date: 1741022248 +0000
Message: Last commit before week-end !
Tree: 7b3d21bc77b2acda6d6c4c94f51a4bb01d6504f8
Files in this commit:
100644 .env (350f10b0c9123e09b88ef2a05fd76848902fd677) - blob
Content: SECRET="404CTF{492f3f38d6b5d3ca859514e250e25ba65935bcdd9f4f40c124b773fe536fee7d}"
100644 app.py (822a5257be1ed6d883e84877f7ba2253b294fa96) - blob
Content: import os
from flask import Flask, render_template
from dotenv import load_dotenv
load_dotenv()
SE...
Commit: c8e66485c89a29768dd546a3046b8544520615d6
Author: Alba Laine <stagiare@docker.flag>
Date: 1741022248 +0000
Message: Source code of website
Tree: fc29f474601267e99d2e9e5861e978fb33de36c9
Files in this commit:
100644 app.py (822a5257be1ed6d883e84877f7ba2253b294fa96) - blob
Content: import os
from flask import Flask, render_template
from dotenv import load_dotenv
load_dotenv()
SE...
Bingo, le flag était bien dans un fichier .env
dont le contenu a été enregistré dans les commits :
404CTF{492f3f38d6b5d3ca859514e250e25ba65935bcdd9f4f40c124b773fe536fee7d}