某税网登录逆向-sm2-HMacSHA256-sm4-滑块
目标网站:
aHR0cHM6Ly90cGFzcy5qaWFuZ3N1LmNoaW5hdGF4Lmdvdi5jbjo4NDQzLyMvbG9naW4=
接口分析:
打开一个无痕窗口,账号密码随便输入,点击登录,手动过掉滑块会发现一共发起了五个请求,挨个点开分析。
getPublicKey:先看载荷,发现只有一个signature参数是加密状态,点开响应发现主要返回了一个uuid,还有一个publicKey密钥。所以这个接口的作用应该是初始化密钥,后面的请求都会以他返回的内容为基础进行操作。
sendSm4:先看载荷,发现有一个signature参数是加密状态,然后datagram中也有两个参数,一个uuid,一个secret,搜索发现uuid就是请求getPublicKey返回的uuid,secret应该是加密生成的,然后发现headers中X-TEMP-INFO也有值了,就是返回的uuid,后续的请求这个值都是一样的。点开响应发现没有返回什么东西,猜测应该也是初始化的请求。
getCaptcha:请求也是datagram和signature进行了加密处理,返回的内容就是滑块图片的相关信息。
verifyCaptcha:请求也是datagram和signature进行了加密处理,返回的内容发现有一个datagram也是属于密文状态,还有一个signature也是密文(后面发现这个是基于hash生成的,不可逆)。
accountLogin:请求也是datagram和signature进行了加密处理。进行登录账号验证。
逆向分析:
在请求参数主要是有两个值datagram和signature有加密操作,需要解密的只有verifyCaptcha返回的内容需要解密。
先看加密,全局搜索signature或者自行跟栈,在下图发现对datagram和signature加密的地方,但代码在进行了判断操作需要注意,主要是判断url是否在S列表中,是就对data进行json序列化,否则就进行加密操作。
"post" === n.method && (S.includes(n.url) ? (o["encryptCode"] = "0",o.datagram = JSON.stringify(n.data)) : (c = JSON.stringify(n.data),u = Object(P["d"])(c, Object(P["i"])(l)),o.datagram = u,o["encryptCode"] = "2")),
查看S列表发现是接口列表,当getPublicKey、sendSm4这两个请求时,不加密,其他请求进行加密。
getPublicKey请求:只有signature是加密的,而signature是由Object(P["a"])(o["zipCode"] + o["encryptCode"] + u + o["timestamp"] + o["signtype"], g),生成,分析参数,发现zipCode,encryptCode, u,signtype为固定值,timestamp为当前时间,g是随机生成的字符串。
Object(P["a"])是HmacSHA256加密。
sendSm4请求:除了signature,还有一个secret,全局搜索或者自行跟栈,发现在下图所示位置进行了处理,A = Object(_utils_getUuid__WEBPACK_IMPORTED_MODULE_5__["c"])(n, localStorage.getItem("naturepublicKey"), 1),其中n为getPublicKey请求中生成的随机字符串,naturepublicKey为getPublicKey请求返回的publicKey,1为加密模式。 Object(_utils_getUuid__WEBPACK_IMPORTED_MODULE_5__["c"])是sm2加密,signature加密方式和getPublicKey请求的一致。
getCaptcha请求:先看datagram,这个值是 u = Object(P["d"])(c, Object(P["i"])(l)生成的,c是固定值,l是getPublicKey请求中生成的随机字符串截取前八位再加上O拼接成的。Object(P["i"])函数是将l转为16进制数据,Object(P["d"])是进行sm4加密,返回结果中blockSrc为滑块图,canvasSrc为背景图,signature加密方式和getPublicKey请求的一致,只是其中的u为datagram的值。
verifyCaptcha请求:先看datagram,发现加密前的原文为有两个值,一个bolck(滑动距离),一个uuid(图片返回的uuid),加密还是使用的sm4加密,signature加密方式和getPublicKey请求的一致,只是其中的u为datagram的值。这里滑块的识别我是使用的ddddocr直接识别的,另外对返回的datagram进行解密,这里我直接搜索的sm4,然后筛选出解密函数(这里密钥还是使用与加密一致的l的值),解密的值为{"ticket":"754e26b0f6a742ed8008260afaf76cc9"}。
accountLogin请求:先看datagram,发现data有四个值,client_id和redirect_uri固定,另外两个参数分别为账号和密码,使用的sm4加密。然后请求观察发现headers中X-TICKET-ID携带了值,对比发现是滑块验证后返回的ticket,signature加密方式和getPublicKey请求的一致,只是其中的u为datagram的值。
逆向分析结束,就还是代码构建,完成请求。
参考代码:
const CryptoJS = require('crypto-js')
const smcrypto = require("sm-crypto");var O = "dt!P^bfrR6LhHTUutGk3GwdKdewgdewgjfekqwgfgfjg".substring(0, 4) + "fwejkfjqfgjgfhewvfvq^R4qd^VLrf^2^ujqgM2Rpb9t".substring("fwejkfjqfgjgfhewvfvq^R4qd^VLrf^2^ujqgM2Rpb9t".length - 4),S = ["/auth/oauth2/getPublicKey", "/auth/white/sendSm4", "/auth/user/logout", "/auth/user/checklogin", "/auth/qrcode/verifyQRCode", "/auth/message/sendSmsCode", "/auth/oauth2/revokeToken", "/auth/white/getAreCode", "/auth/white/getSecondAuthInfo", "/auth/oauth2/checkRedirectUrl", "/auth/message/captchaImage"];
Date.prototype.format = function (e) {var n = {"M+": this.getMonth() + 1,"d+": this.getDate(),"h+": this.getHours() % 12 == 0 ? 12 : this.getHours() % 12,"H+": this.getHours(),"m+": this.getMinutes(),"s+": this.getSeconds(),"q+": Math.floor((this.getMonth() + 3) / 3),S: this.getMilliseconds()}, A = {0: "日",1: "一",2: "二",3: "三",4: "四",5: "五",6: "六"};for (var t in /(y+)/.test(e) && (e = e.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length))),/(E+)/.test(e) && (e = e.replace(RegExp.$1, (RegExp.$1.length > 1 ? RegExp.$1.length > 2 ? "星期" : "周" : "") + A[this.getDay() + ""])),n)new RegExp("(" + t + ")").test(e) && (e = e.replace(RegExp.$1, 1 == RegExp.$1.length ? n[t] : ("00" + n[t]).substr(("" + n[t]).length)));return e
}function p_a(e, n) {var A = CryptoJS.HmacSHA256(e, n).toString();return A
}function v(e, n) {var A, t, f = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(""), v = [];if (n = n || f.length,e)for (A = 0; A < e; A++)v[A] = f[0 | Math.random() * n];elsefor (v[8] = v[13] = v[18] = v[23] = "-",v[14] = "4",A = 0; A < 36; A++)v[A] || (t = 0 | 16 * Math.random(),v[A] = f[19 == A ? 3 & t | 8 : t]);return v.join("")
}function p_b() {return p = v(16, 61),p
}function p_i(e) {for (var n = "", A = 0; A < e.length; A++)"" === n ? n = e.charCodeAt(A).toString(16) : n += e.charCodeAt(A).toString(16);return n
}
function p_d(e, n) {var t = smcrypto.sm4, f = t.encrypt(e, n);return f}function get_params(data, t, flag) {o = {}, u = ""o["zipCode"] = "0"g = t || p_b(), l = g.substring(0, 8) + Oif (!flag) {o["encryptCode"] = "0"o.datagram = JSON.stringify(data)} else {c = JSON.stringify(data),u = p_d(c, p_i(l)),o.datagram = u,o["encryptCode"] = "2"}o["timestamp"] = (new Date).format("yyyyMMddHHmmss"),o["access_token"] = "",o["signtype"] = "HMacSHA256"o["signature"] = p_a(o["zipCode"] + o["encryptCode"] + u + o["timestamp"] + o["signtype"], g)return {o: o, g: g}
}function sm4_decrypt(e, n) {var a = n.substring(0, 8) + O;var t = smcrypto.sm4, f = t.decrypt(e, p_i(a));return f}function a(e, n, t) {var f = smcrypto.sm2;if (n) {var v = f.doEncrypt(e, n, t);return v}return ""
}function get_secret(uuid, g, publicKey) {var A = a(g, publicKey, 1)return {uuid: uuid,secret: A}
}
import base64
import timeimport requests
import json
import execjs
from loguru import logger
import ddddocr
session = requests.Session()
with open('tpass_jiangsu_chinatax.js', 'r', encoding='utf-8') as f:js_func = execjs.compile(f.read())def slide_ocr(target, backgroup):det = ddddocr.DdddOcr(det=False, show_ad=False, ocr=False)length = det.slide_match(target_bytes=base64.b64decode(target.split(',')[-1]), background_bytes=base64.b64decode(backgroup.split(',')[-1]))return lengthheaders = {"Accept": "application/json, text/plain, */*","Accept-Language": "zh-CN,zh;q=0.9","Authorization;": "","Connection": "keep-alive","Content-Type": "application/json","Origin": "https://tpass.jiangsu.chinatax.gov.cn:8443","Referer": "https://tpass.jiangsu.chinatax.gov.cn:8443/","Sec-Fetch-Dest": "empty","Sec-Fetch-Mode": "cors","Sec-Fetch-Site": "same-origin","User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36","X-APP-CLIENTID;": "mcsc7e2ssscb4sfmbsmas35sass2753b","X-LANG-ID": "null","X-NATURE-IP;": "","X-SM4-INFO": "0","X-TEMP-INFO": "null","X-TICKET-ID": "null","deviceIdentyNo": "TgEpmJez7xAPayFJCqgyKKWCt1TrUl6K","hUid;": "","sec-ch-ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"","sec-ch-ua-mobile": "?0","sec-ch-ua-platform": "\"Windows\""
}
url = "https://tpass.jiangsu.chinatax.gov.cn:8443/sys-api/v1.0/auth/oauth2/getPublicKey"
getPublicKey_data = {}
params = js_func.call('get_params', getPublicKey_data)
t = params['g']
logger.success(f'随机数t:{t}')
data = params['o']
data = json.dumps(data, separators=(',', ':'))
getPublicKey_response = session.post(url, headers=headers, data=data)
logger.info(f'getPublicKey请求结果:{getPublicKey_response.text}')
uuid = json.loads(getPublicKey_response.json()['datagram'])['uuid']
headers['X-TEMP-INFO'] = uuid
logger.success(f'uuid:{uuid}')
publicKey = json.loads(getPublicKey_response.json()['datagram'])['publicKey']
logger.success(f'publicKey:{publicKey}')
url = "https://tpass.jiangsu.chinatax.gov.cn:8443/sys-api/v1.0/auth/white/sendSm4"
sendSm4_data = js_func.call('get_secret', uuid, t, publicKey)
logger.success(f'sendSm4_data:{sendSm4_data}')
data = js_func.call('get_params', sendSm4_data, t)
data = json.dumps(data['o'], separators=(',', ':'))
sendSm4_response = session.post(url, headers=headers, data=data)
logger.info(f'sendSm4请求结果:{sendSm4_response.text}')
url = "https://tpass.jiangsu.chinatax.gov.cn:8443/sys-api/v1.0/auth/captcha/getCaptcha"
getCaptcha_data = {"client_id": "mcsc7e2ssscb4sfmbsmas35sass2753b","redirect_uri": "https://etax.jiangsu.chinatax.gov.cn:8443/mhzx/api/mh/tpass/code"
}
data = js_func.call('get_params', getCaptcha_data, t, 1)
data = json.dumps(data['o'], separators=(',', ':'))
getCaptcha_response = session.post(url, headers=headers, data=data)
logger.success(f'getCaptcha_data请求结果:{getCaptcha_response.text}')
img_list = json.loads(getCaptcha_response.json()['datagram'])
slide_length = slide_ocr(img_list['blockSrc'], img_list['canvasSrc'])
logger.success(f'滑块识别结果:{slide_length}')
url = "https://tpass.jiangsu.chinatax.gov.cn:8443/sys-api/v1.0/auth/captcha/verifyCaptcha"
verifyCaptcha_data = {"blockX": slide_length['target'][0],"uuid": img_list['uuid'],
}
data = js_func.call('get_params', verifyCaptcha_data, t, 1)
data = json.dumps(data['o'], separators=(',', ':'))
verifyCaptcha_response = session.post(url, headers=headers, data=data)
logger.success(f'滑块验证结果:{verifyCaptcha_response.text}')
ticket = js_func.call('sm4_decrypt', verifyCaptcha_response.json()['datagram'], t)
logger.success(f'ticket:{ticket}')
headers['X-TICKET-ID'] = json.loads(ticket)['ticket']
url = "https://tpass.jiangsu.chinatax.gov.cn:8443/sys-api/v1.0/auth/user/first/accountLogin"
accountLogin_data ={"client_id": "mcsc7e2ssscb4sfmbsmas35sass2753b","redirect_uri": "https://etax.jiangsu.chinatax.gov.cn:8443/mhzx/api/mh/tpass/code","account": "12345678911","password": "123456"
}
data = js_func.call('get_params', accountLogin_data, t, 1)
data = json.dumps(data['o'], separators=(',', ':'))
response = session.post(url, headers=headers, data=data)
print(response.text)
结果: