web3+web2安全/前端/钱包/合约测试思路——尝试前端绕过直接上链寻找漏洞
0x01 前言
DEFI APP会存在许多的前端限制,原因是一些项目常常会有多种多样的限制,然而DEFI APP有别于传统的JAVA后端语言,DAPP有自己独属于区块链特性,能够直接交互上链且上链后不可篡改特征也让DAPP上线前往往更加严格,在前端的限制有可能只是为了掩盖该项目合约的问题(项目合约可编译不公开代码,可进行白名单KYC认证)
因为我们也可以通过绕过前端来寻找该合约问题,可能会发现一些小的问题,也可能发现不到问题,但是是一种有效的测试方案。
0x02 场景设置
在加密货币和去中心化金融(DeFi)领域,添加流动性后不再允许用户进行交易(添加流动性等于意味着自己是项目方的一部份),一般来说能够流动性提供者能够获得这个流动池里交易所拿到的手续费(根据获得的LP流动性代币份额比例计算)
可能某些时候项目方认为你既是提供者又是交易者,不允许你既能通过交易获取额外收益的同时,又能享受其他用户的交易获取一定比例的手续费。
如图所示,我在该DAPP的该项目提供了流动性池代币,现在就无法让我再次进行交易。
0x03 尝试绕过
由于一点击按钮,就提示该错误"无法下AMM订单",因此尝试在前端进行绕过,直接搜索关键词。关键词的键值为:PC_Trade_Common_01_6
定位到关键的js代码
发现这个值的触发来源于如下,也就是说进入了以下方法后,通过switch来判断,最终达到case为15的时候,就会进入判断,那么我们只需在断点时候不要让他进入这个判断
methods: {
handlePlaceOrder: function() {
var e = this;
return Object(l["a"])(Object(u["a"])().mark((function t() {
var n, a, i, r, s, o, _, c, l, d, C, p, m, f, P;
return Object(u["a"])().wrap((function(t) {
while (1)
switch (t.prev = t.next) {
case 0:
if ("" !== e.account) {
t.next = 3;
break
}
return e.$emit("connectWallet"),
t.abrupt("return");
case 3:
if (e.curPair.tradeType === S["k"].allTrade) {
t.next = 6;
break
}
return e.$notify({
title: e.$t("PC_Common_PopUp_01_15"),
message: e.$t("PC_Trade_AMM_01_3"),
type: "warning",
duration: 5e3,
customClass: "warning-notify"
}),
t.abrupt("return");
case 6:
if (void 0 !== e.userWalletInfo.status) {
t.next = 9;
break
}
return k["a"].$emit("showBlackListTip"),
t.abrupt("return");
case 9:
if ("buy" !== e.tabType || 4 == e.userWalletInfo.status) {
t.next = 12;
break
}
return k["a"].$emit("showBlackListTip"),
t.abrupt("return");
case 12:
if (!("sell" === e.tabType && e.userWalletInfo.status < 4)) {
t.next = 15;
break
}
return k["a"].$emit("showBlackListTip"),
t.abrupt("return");
case 15:
if (!(e.liqBalance > 0)) {
t.next = 18;
break
}
return e.$notify({
title: e.$t("PC_Common_PopUp_01_33"),
message: e.$t("PC_Trade_Common_01_6"),
type: "warning",
duration: 5e3,
customClass: "warning-notify"
}),
t.abrupt("return"
发现是通过这个t.next字段来不断判断进入执行的JS,执行顺序为3-6-9-12-15-18....
所以我们只需修改断点JS,让他直接跳过case15的判断即可,F8,不断让他直接变为18,这样就不会进入"AMM流动池判断"的报错
此时已经成功进入合约交互,只不过合约此时还是做了限制,所以该笔交易被拒绝上链。
这种方法只是尝试Web3 DAPP安全测试的一种手段和思路。
0x04 案例二
例如基金,理财产品,某些理财需要至少50W元才能够申购,因此该DAPP也设置了某些项目至少需要最小的金额申购。
搜索订阅数量必须大于等于最小值——PC_Offering_SubRed_02_26
tradeConfig: {
coinList: [],
txParams: {},
clickEvt: function() {
var e = Object(l["a"])(Object(o["a"])().mark((function e(n) {
var i, r, l, c, u, d, h, p, f, g, v, m, y, _, b, x, C, w, S, O, I, k, P, j, L, R;
return Object(o["a"])().wrap((function(e) {
while (1)
switch (e.prev = e.next) {
case 0:
if (t.validateAmount(),
i = t.error,
r = i.inputError,
l = i.inputMaxError,
c = i.inputMinError,
u = i.inputUnitError,
!(r || l || c || u)) {
e.next = 7;
break
}
if (!r) {
e.next = 5;
break
}
return e.abrupt("return", !1);
case 5:
return c && t.$notify({
title: t.$t("PC_Common_PopUp_01_33"),
message: t.$t("PC_Offering_SubRed_02_26"),
type: "warning",
customClass: "warning-notify"
}),
e.abrupt("return", !1);
case 7:
return d = t.detail,
h = d.chainId,
p = d.tokenCode,
e.next = 10,
Promise.all([Object(A["c"])(t.nowCoin, h), Object(A["c"])(p, h), Object(A["c"])("IssueManagement", h)]);
寻找到PC_Offering_SubRed_02_26的地方为case 5,按照上面的思路,要绕过这个判断我们只需直接让他调到case 7即可。
我们只需在每一步都修改为7即可(前面的几步可能修改无法影响next值,只是为了方便,需无脑修改即可)
修改为7
成功进入7
弹出确认框
点击确认后,依旧进入刚刚的判断重复,我们只需依旧无脑修改next为7即可
之后就进入申购环节,绕过了"搜索订阅数量必须大于等于最小值"
之后直接关闭断点,或者F8持续即可,看看合约给我们的反馈如何。
幸运的是,这次合约接受了我们发起的交易,也就是我们这笔订单是成功的上链了
但是实际上,我经过多次验证后确认,该笔订单就算成功上链了,也不一定能够证明存在"逻辑漏洞"———不按照规定的标准量申购理财产品。
这笔订单上链了,只能证明我们的钱被合约系统接受了,但是实际上DAPP尤其是RWA(Real World Assets,现实世界资产)是比较高风险的DAPP,因此系统可能在链下也同时进行了校验。
回到上面所说的这笔订单,虽然是上链成功了,但是由于订单会反馈到后台,后台实际上是会检测异常的,在数据库里返回exception,所以说这个漏洞利用失败了,但是这同样是针对DAPP网站的一种非常有效的漏洞利用思路。