又一次问世了,我的博客,不知写什么为好。总之,你好,世界!
又一次问世了,我的博客,不知写什么为好。总之,你好,世界!
小六推荐了这个靶场,好久没玩玩靶场了,扫了一眼,题目都是比较感兴趣的,打一打,顺便总结整理一下平时挖洞时思路与技巧。前段时间心血来潮在看GoF设计模式,花费了很多时间用来学习和练习,看这个进度也是要等好久才能输出关于设计模式的文章了,安卓逆向进度也放一放,慢慢来吧。
既然是SRC,那这个系列就点到为止,尽量不使用自动化工具,手工证明即可,不以拿flag为目标。
这里的支付漏洞还是相当简单的,也是面试中的常被问到的点。
做这道题之前,需要了解一些基础的数值的数据类型:
| 类型 | 符号性 | 字节数 | 数值范围 |
|---|---|---|---|
| int8 | 有符号 | 1 | -128 ~ 127 |
| uint8 | 无符号 | 1 | 0 ~ 255 |
| int16 | 有符号 | 2 | -32768 ~ 32767 |
| uint16 | 无符号 | 2 | 0 ~ 65535 |
| int32 | 有符号 | 4 | -2147483648 ~ 2147483647 |
| uint32 | 无符号 | 4 | 0 ~ 4294967295 |
| int64 | 有符号 | 8 | -9223372036854775808 ~ 9223372036854775807 |
| uint64 | 无符号 | 8 | 0 ~ 18446744073709551615 |
可以参考一下月神的这个科普视频: 【最大值溢出漏洞挖掘科普-月神】
就拿int8来举例子吧,有符号的数是通过补码的最高位来表示正负,1是负数,0是正数,那么int8类型的最大值就是0111 1111,如果在这个基础上+1的话,就变成了1000 0000,就是负数中的最小值,通过补码转换就是-128,所以就会出现,int8类型的127+1=-128,然后还有一种情况,这种是正常的补码运算,就是-1 + 1,也就是1111 1111 + 0000 0001 = 1 0000 0000,进位舍弃,结果为0000 0000,也就是0。所以,整数数值就相当于首尾相连。
来分析分析这个场景吧:
现在我们有1000元,目标商品是2400元,通过金额整数溢出来达成购买商品。



分析一下,这里上传了数量,我们能控制的有用参数貌似只有数量,商品单价是2400,看一下参数有什么限制。

这里限制死了,只能上传大于等于0的参数,这里尝试上传一个大数
推测这里使用的是int32,月神那个视频里也提及了,微信支付使用的也是int32。反复测试一下,总金额没有超过2147483647,尝试进行刚刚好溢出,可能付款的值就小于当前值,数量为q, 2400*q -> 2147483647+2147483648,计算出q的值取整然后+1。

这个漏洞比较简单啊,成因是上送了优惠券的个数并进行计算,未对购买者持有优惠券数量进行校验。

这个漏洞也比较简单,上送了支付金额,推荐后端开发时,不进行上送利率、金额、总价等,只上送产品的id、个数并进行严格的过滤。


这个的思路也十分的清晰,他的交易逻辑就是先上送产品id和数量,生成订单号,然后通过订单号进行扣费,这里能改的也就是数量,测试一下数量是否做了校验。



感觉没有记录的必要。。。 
有问题啊这个靶场,领不了优惠券,但是可以用订单编号那个过,有点失望。。。
很简单啊,id越权,然后解JWT,这算非预期?


很简单的一个id越权,简单的洞,但是很常见。

本靶场需要进行简单的js逆向,挖掘越权查看他人敏感信息漏洞。
来,让我看看是什么js逆向,我觉得都难不倒我。

这里一看就是一个对称加密,找到算法和key就好了。
找到了加密文件:
1 | ;(function(_0x2f4d, _0x5b7e, _0x8bf6) { |

传入的openid是_0x5d31,在78行打断点,然后将其修改成1即可进行越权查询。
靶场目标:买下会员专属商品
由于需要手机号验证码登录,所以做了一个验证码平台在8081端口
提示:此环境组合了两种手法,包括一种基础的手法与前面的靶场:《有个接口隐藏了,找到它》的练习手法,靶场不存在弱口令与验证码爆破,请勿浪费性能
重要:验证码平台不参与漏洞逻辑,仅提供接码功能,预期解不涉及验证码平台本身,也不根据验证码平台能提供的信息去猜测
这题的信息如上,反正我是没找到隐藏接口,太菜了,这种拼接谁能想到啊,将8081的接口拼到80的api上,我服了。

然后用获取到的糖糖账号,登录,更改获取手机验证码的接口参数。

登录后购买,这谁能想到啊,我要给差评。

简单的任意用户登录
滴滴滴滴,你收到了一个电话,来自13900812231,哦原来是四川成都xx单位的客户,他发给你了一个网站,让你测试有没有漏洞?

既然题目都给出了任意用户登录了,我猜应该是响应包文直接替换就能绕过,尝试了一下,替换并不能直接进去,也不存在未授权,那么接下来看一下这个忘记密码功能和注册功能,任意用户登录在这里也容易出现。
确实可以通过修改响应包来实现跨步,直接修改账号的密码。

然后我尝试修改了题目中给出的手机号,可以修改,但是登录仍然显示封禁中,在我没有思路的时候,我去看了一眼评论区的提示,手机号可以爆破出可以使用的账号

按照刚才的思路,重置一下这个账号,并进行登录



篡改响应包,在请求包中加入主页下方的管理员手机号,别问我为什么这样绕,问出题人棉花糖吧。


一眼排序注入


拿flag的话直接拿sqlmap跑吧,这里证明一下存在注入点就可以走了。

依旧圣杯页,注册一个账号

这个棉花糖怎么这么不正经。。。
这里说是xss+csrf,接管账号的话,直接去找重置密码接口,看看密码重置接口需要的参数是什么。

这里重置密码做的很敷衍啊,很简单,直接开始构造xss吧。

重新创建一个账号,看看是否存在csrf。确实可以更改

这里也存在糖糖的账号,直接登录试试。

也是成功登录上糖糖的账号。
直接扫目录,扫到了swagger,拼接接口(这个我是真的想不到得拼接admin,看了评论区才知道要这样整,还是太菜了)

这个SRC靶场就这么打完了,这个靶场还是比较简单的,但有些点确实想不到,多练吧,没别的说的,菜就多练。
安卓开发分SDK和NDK,SDK运行在ART/Dalvik虚拟机中,NDK运行在原生系统中,虚拟机执行效率不如原生效率高,要谈两者的区别,还是直接放一个SDK调用NDK的图,通过实际的调用流程来了解一下吧。
Zygote 是安卓系统启动时就会创建这么一个进程,它是所有安卓应用的父进程。Zygote在启动时,会加载系统核心框架库(如framework.jar,包含Activity、Service等核心类的实现),每启动一个应用,Zygote就会通过Fork自身来生成新进程,前面那些预加载的资源就会被所有子进程共享,避免重复加载。
所以Zygote是创建ART/Dalvik虚拟机的进程,它会完成虚拟机的初始化,确保fork出来的应用进程都继承一个准备就绪的、标准化的虚拟机环境。应用启动后,就会在这个虚拟机中加载APK中的Dex码,进行初始化Application,然后就是四大组件的初始化。
那图中的JNI又是个啥呢,在安卓中,JNI 是指 Java 原生接口,是一个规范。它定义了 Android 从受管理代码(使用 Java 或 Kotlin 编程语言编写)编译的字节码与原生代码(使用 C/C++ 编写)互动的方式。SDK调用NDK,是通过JNI与NDK进行调用映射和数据类型转换等操作,完成JAVA层到NATIVE层的通信。
话说回来,NDK就是为了高运行效率、跨平台复用、底层逻辑操作,按照JNI规范在原生空间中运行编译C\C++代码的Android原生开发工具集。
扔一点命令
1 | //查看当前界面Activity |
通过Android Studio创建一个Native开发项目,在java层中写进行声明
1 | public static native String mdString(String string); |
对应的,native层的定义(C版)
1 | JNIEXPORT jstring JNICALL |
其中,如果是已CPP进行native层的编写,就需要在最开始进行一个name mangling,因为CPP可能会进行重载,抽空去研究一下编译区别(咕咕咕)。
1 | extern "C" JNIEXPORT jstring JNICALL |
| Java 基本类型 | JNI 对应类型 | C/C++ 等效类型 | 占用字节 | 说明 |
|---|---|---|---|---|
| boolean | jboolean | unsigned char | 1 | 0=false,非0=true |
| byte | jbyte | signed char | 1 | 8位有符号整数 |
| char | jchar | unsigned short | 2 | UTF-16编码的字符 |
| short | jshort | short | 2 | 16位有符号整数 |
| int | jint | int | 4 | 32位有符号整数 |
| long | jlong | long long / int64_t | 8 | 64位有符号整数 |
| float | jfloat | float | 4 | 32位浮点数 |
| double | jdouble | double | 8 | 64位浮点数 |
| Java 引用类型 | JNI 对应类型 | 说明/使用场景 |
|---|---|---|
| Object | jobject | 所有引用类型的父类 |
| Class | jclass | 对应 Java 的 Class 对象 (用于静态方法/反射) |
| String | jstring | 对应 Java 的 String |
| Throwable | jthrowable | 对应 Java 的异常类 |
| Object[] | jobjectArray | 对象数组 (如 String[], User[]) |
| boolean[] | jbooleanArray | 布尔数组 |
| byte[] | jbyteArray | 字节数组 (常用作二进制数据传输) |
| char[] | jcharArray | 字符数组 |
| short[] | jshortArray | 短整型数组 |
| int[] | jintArray | 整型数组 |
| long[] | jlongArray | 长整型数组 |
| float[] | jfloatArray | 浮点数组 |
| double[] | jdoubleArray | 双精度浮点数组 |
| 自定义类/接口 | jobject | 统统映射为 jobject |
| 用途 | JNI 类型 | 说明 |
|---|---|---|
| JNI 环境指针 | JNIEnv* | 核心接口指针 (C使用 (*env)->, C++使用 env->) |
| 虚拟机指针 | JavaVM* | 跨线程获取 JNIEnv 时使用 |
| 字段 ID | jfieldID | 访问 Java 属性的凭据 |
| 方法 ID | jmethodID | 调用 Java 方法的凭据 |
直接抄仨对照表扔这,用到的时候再看。
写完对应的代码,再扔一个命令行编译的命令
1 | ./gradlew assembleDebug |
这里使用frida进行native层的被动调用,需要找到要hook的libso名称,还有要hook的函数名称,以及入参和出参类型,使用以下命令进行查找。
1 | Using USB device `KB2000` |
找到我们想hook的JNI,通过ida或者c++filt工具查看传参

由于该jni是在启动时后就进行直接调用的,这里改用了lasting_yang师傅写的 frida_dump去hookdlopen,非常给力的fridahook项目,抽空研究研究(咕咕咕~)
1 | var can_hook_libart = false; |
1 | setTimeout(()=>{ |

JSVMP(JavaScript Virtual Machine-Based Protect)JS虚拟机保护,VMP技术就是将原有的代码,转化为专用的字节码,由自写解释器进行解释执行,以此大幅提升逆向分析的成本,同样的,由于代码量的增加,运行效率也会大打折扣。
这里谈一谈一个概念,解释执行,就是将源代码逐行转换为机器码然后再执行。类似于JAVA的JVM,将字节码转换成机器码再执行,但是在JSVMP中,这里的的机器码就不再是可供CPU执行的x86/ARM指令,而是我们自定义的虚拟指令序列,实质上还是JS代码。
之前在windows软件、安卓native层中也遇到过VMP加壳,原理完全相同,比较亲切,但是在保护的对象和实际技术应用上,有着很大的区别,这里就不细谈了,等后续博客更新到安卓逆向或者windows 逆向,可能会拿在一起详细讲讲(??哪来的鸽子叫)。
JSVMP加密技术:原理、实现与攻防策略解析
这篇文章讲的就很不错了,聊个题外话,我一直认为,不论是写技术博客,还是分享CTF的WP等等分享,都需要写一点自己的内容,哪怕你看完别人的文章,按照别人的思路,自己重新复述一遍,那么这篇文章就是有价值的。不要为了完成一篇文章,去写一些自己不清楚的东西,复制粘贴更甚,不要这么做,要真真正正留下来点东西,最不重要的就是毫无意义的文字和浪费掉的时间。
接下来我将写一个简单的JSVMP,来演示一下他的执行流程,就最简单的,二数加法运算。实现之前,让我来讲一讲大体思路:
先准备一下指令序列,指令序列按照一定的规则进行保存,可以是特定的编码,这里规定操作码和参数都以join(“/“)进行拼接。先生成一个指令序列以便后续解释器使用。
1 | var instructions = [ |
1 | (base) ➜ node demo.js |
这样一个混淆后的ADD指令集就生成好了,接下来实现一下最关键的指令集解析部分
1 | // 创建堆栈和指令集,指令集加一个混淆,通过this.process进行动态加载,这里先置空,js中使用list来模拟stack非常的方便 |
1 | (base) ➜ jsvmp node demo.js |
结果是对的,这个简单的ADD VMP就已经实现了,自定义了指令集、解释器,还具备了一定的混淆。
JSVMP的特征就是:
存在以上结构
存在JSVMP字样入参传入很长的一串字符串
根据前人的经验,JSVMP大概有三种方法进行处理,第一种就是直接补环境,第二种就是纯算还原,第三种就是使用RPC进行远程调用。补环境比较方便,直接来像之前那样来补环境吧。

拿一个字节的网站来看看他们是怎么做的JSVMP,关注到,这里有一个_signature参数,来看一下这个是怎么生成的,打断点调试一下。

调试到,是有一个windows.byted_acrawler.sign函数用来生成这个signature值,走进去看一看有什么东西。

很明显,这里的signature生成逻辑已经被JSVMP混淆隐藏了,要在本地运行这个sign函数,可以将整个JSVMP代码全部扣下来,然后进行补环境。根据这个文件名可以发现,这个acrawler.js文件,是存在反爬的,可能会有环境检测,根据经验,进行补环境的过程中需要做一些特别的处理。
浏览器补环境,在node环境中要特别注意一下两个东西,一个是exports,一个是module,这两个在浏览器环境中是undefined,但是在node环境中不是,这里反爬会在这里区别一下环境,防止本地运行,需要手动调整一下代码。
1 | "undefined" != typeof exports ? exports : void 0, "undefined" != typeof module ? module : void 0, |

补环境中经常会遇到这样的length缺失,这就需要找对应上一步的A参数是什么,在对应的地方下一个log断点,和一个条件断点,查看length上一步是做了什么操作,然后再通过条件断点查看需要补什么环境。

然后使用调试,查看上一步的参数是protocol

重新设置条件断点 A==”protocol”,查看需要补的参数,通过断点发现,这个protocol是location里面的东西,直接从浏览器中拿就好

node环境中又出现了错误,这里可以打印出所有的A参数,查看哪些环境是我们忽略掉的,导致位数不一样,浏览器中的signature参数的位数是147,这里是47,显然补环境还没有结束

经过一步一步的S、R、A参数分析,发现缺少了localStorage和sessionStorage参数,以及对应的操作方法。这里遇到的问题比较多,有一段时间,我以为是我node版本的问题,结果重新走了一遍,通了。总之,要耐心,一步一步来,缺啥补啥。
扔一个补的环境代码:
1 | var document = { |
这里有很多补环境用的Proxy自吐脚本,扔一个,调试起来比较方便
1 | function getEnv(proxy_array) { |
至于纯算嘛,扔个链接:https://mp.weixin.qq.com/s/YAL4iT3ECQeK7vZRsLRFaA ,日后研究(!??哪来的鸽子叫)
还得扔点啥,哦,RPC,自个搜搜Chrome RPC 或者 JsRPC怎么用吧。
讲OB混淆前,先要讲讲什么是混淆,混淆就是在不影响原本的代码的逻辑情况下对代码的形式和结构进行变动,使代码变得难以阅读和理解,增加代码逆向的难度。
在JavaScript中,OB混淆一般是指的网站 https://obfuscator.io 提供的代码混淆,这里面提供了一个JavaScript代码混淆工具,可进行多种类型的代码混淆。
OB混淆基本上由四个部分组成:
1 | - 一个大数组或者有一个大数组的函数 //用于存储加密的字符串和关键数据 |
这是OB混淆代码的基本构成,OB混淆有如下识别特征:
1 | //变量名和函数名混淆 ,形如 _0xab1cd2。由"_0x"开头、无规律字母数字开头,后面跟上1-6位数字字母组合。 |
纸上得来终觉浅,动手试一试才能知道OB混淆前后的改动:
1 | //混淆前的代码 |
上面的代码就是一个标准的四个模块,让我来给拆解一下:
1 | //第一块是解密函数,可以看到里面直接调用了大数组函数 |
既然有工具能够直接生成OB混淆,那也有工具能直接解OB混淆
这个工具整一下午没整好,有点失望,我后续将补充这部分内容,暂且搁置。
这里就跟OB混淆关系不是很大了,只需要将OB混淆后的部分,全部copy下来,简单替换几个函数就可以了,主要是耐心,
OB混淆只不过是混淆之万一,用的多了,将规律记录下来,下次遇到便会得心应手。
某公众号功能

这里用的是真机adb调试Chrome inspect调试,针对不能开启f12的公众号、内嵌webview的APP还是挺好用的,公众号使用的话,还需要在微信输入并点击:
1 | http://debugxweb.qq.com/?inspector=true |

然后就可以进去直接调试,还需要再绕过一下无限debugger,这个网上文章有很多,这里用的是这个修改原型链:
1 | var _constructor = constructor; |

可以发现这里,请求荷载被加密了,打断点跟一下,看看在哪里处理加密解密的

来看看这个函数,使用了经典的平坦流控制进行关键函数的混淆,也就是while true 和 switch 进行代码执行流程的混淆,只需要打断点,然后把执行的代码,一行一行拼一起,不过我们只在乎请求报文加密,这里可以忽视。
1 | function encryptParamNEW(_$H, _$l, _$n) { |
简单看一下,这个函数是用来加mac的,进行报文防篡改的,可以pass了。


很清楚的看到,报文被加密了,这个ob混淆很简单呢,加密函数一下就能找到,也不用处理大数组等东西,算是一个失败的例子。
然后再进行代码本地化。

补环境过程中,遇到这种本地不能被解析的,可以直接去调试,看看这里是什么,直接替换成明文。因为这个getPublicKey函数比较短,环境不变的情况下,一般只有一个返回值,这里就可以自行判断,只留下return的值,也是个不错的补环境方法。
其中有一个反调试函数,可以尝试将它删除
1 | function _$o(_$H) { |
将这两个部分的代码注释掉就可以,接下来就是很基础的补环境,还有ob混淆的补环境。大体就是这个流程。

纸老虎,看着挺多挺唬人,其实很简单,麻烦一点,要耐心。
是一个javascript静态打包工具,他的作用是将js代码进行分割,模块化,提高js的加载效率。
下面是webpack的特征:
1 | //文件开头存明显的webpack字样 |
Webpack使用闭包函数都会用到加载器,下面来讲一讲加载器长什么样:
1 | !function(e) {...}([...]); |
再写一个更为详细的例子:
1 | !function(e){ |
逆向webpack代码的痛点在于,代码执行流程不清晰,webpack将模块包装在闭包中,模块间复杂的函数依赖。解决上述问题就需要:
上面写的例子就比较简单了,代码执行流程比较容易分析,真实情况可能是,存在多个JS文件,每个JS文件中都有很多个模块,加载器不一定只在存在于模块上方。
找到加载器和模块后,按照加载器加载模块,将需要用到的模块填入传参中,再用一个变量将加载器导出,就能直接调用函数链了。
网址
用到的工具环境:
IDE: WebStorm (方便折叠代码,vscode、notepad++、notepadNext都可以)
浏览器:Chrome Devtools

这里看到传入的密码是加密的,打断点进行调试,最终找到加密是在app.939ea5e9.js中“MuMZ”模块中的r函数。找到我们的目标函数了。

接下来找加载器位置,往上翻一翻,根据前人的经验,遇到xxx.m就可能是加载器了,直接去看c。

就是这个,是不是很像一开始的例子。

把整个代码复制出来,折叠一下,就一目了然了。

删减代码,然后尝试加载所需的模块,存在报错,在加载器中加一段调试代码,看看缺少哪些模块。

这样就能知道,问题出在哪里。

补了一共51个模块,不报错了,最后查看一下模块内部的导出名,是b,运行一下,没报错。
