CTF高手教你如何实现文件加解密破解 | 您所在的位置:网站首页 › versioncode翻译 › CTF高手教你如何实现文件加解密破解 |
0x01
前几日,在公司对客户单位的网站与APP进行授权渗透,在使用用户提供账号登录APP端进行抓包时,发现APP传输包与返回包均进行了加密 bp抓包界面如下: 为破解密文获取请求与传输包中内容,于是便对APK文件进行逆向 0x02 APP逆向寻找破解点使用jadx 打开 APK 文件, 搜索发送加密请求接口,寻找加密方式 跟踪到请求接口,发现请求接口加密使用注解@Encrypt,未找到直接的请求包加密方式,在jadx中搜索Request,找到 RequestDataBuilder类 该类中包含有请求包加密解密函数,并含有一个APP_KEY 在主管帮助下在github上获取到一个js文件 因该文件为js文件,且客户单位给的网站与APP同样进行了数据加密,猜测客户单位的网站应该是使用了该js文件进行传输数据加解密,于是根据该js文件尝试对客户单位网站数据加密进行破解 0x03 网站js加密破解对该JS文件进行分析,发现使用该JS文件会发送两种类型请求,登录请求 和 登录后发送的普通请求 , 对应函数如下: 登录请求登录请求会调用login,首先对登录函数login进行分析,该函数传值并调用generateKey函数 /** * 登录函数 * @param username 会员号 * @param pass 密码 */ export function login(username, pass, resultfunc) { generateKey(username, function(obj) { dologin(obj, username, pass, resultfunc); }, function(info) { resultfunc(false, info); }); } /* * 在这个login函数中传入username变量和两个function函数到generateKey函数中 * 因为在JavaScript中函数是第一公民,所以可以作为变量传递 */ login函数传参到generateKey函数后,分析generateKey函数逻辑 /** * 发送请求到服务端,进行秘钥的产生,产生的秘钥包括RSA密钥对,和进行秘钥交换通讯的会话秘钥 * @param tjid * @param successfunc * @param errorfunc */ function generateKey(tjid, successfunc, errorfunc) { $.ajax({ type: "get", url: nctUrl + "/gen/key", data: { "tjid": tjid }, dataType: "json", success: function(obj) { successfunc(obj); }, error: function(req, info) { errorfunc(info); } }); } /* * generateKey函数通过get请求向接口 /gen/key 发起请求,传入参数为tjid * tjid参数其实就是登录网站时输入的用户名 * 如果请求成功, 则调用第一个传入generateKey函数的函数,就是调用dologin函数 * 如果请求失败, 则调用第二个传入generateKey函数,就是resultfunc函数 */ // resultfunc函数其实也是传入的一个函数变量,猜测传入的函数应该为failfunc函数 // 该函数作用其实就是alert一下错误信息,如下 function failfunc(info) { alert(info); } // 总结一下, generateKey函数的作用就是拿用户输入的用户名去跟服务器请求会话秘钥 // 如果成功调用dologin函数, 如果失败alert弹出错误信息 调用 /gen/key 接口如下 如果tjid存在,也就是用户名存在,则请求成功,获取到服务器返回的 ss 和 ps两个值。 当generateKey函数发起请求成功后,则调用dologin函数,下面查看dologin函数 /*** * 执行登录 * @param obj * @param username * @param pass */ function dologin(obj, username, pass, resultfunc) { if (obj.code == 1) { var sessionkey = obj.data.ss; $.ajax({ url: nctUrl + "/login", type: "post", data: { "username": username, "userpass": pass, "publickey": obj.data.ps }, dataType: "json", beforeSend: function(xhr) { addHeaders(xhr); }, error: function(xhr, info) { resultfunc(false, info); }, success: function(obj) { if (obj.code == 1) { var secretkey = obj.data.secretkey; var cryptedInfo = obj.data.info; decryptSecretKey(username, sessionkey, secretkey, function(retObj) { if (retObj.code == 1) { sessionKey = decodeAndDecryptWith3Des(sessionkey, retObj.data); var decoded = decode(cryptedInfo); var plainInfo = decodeAndDecryptWith3Des(produceSessionKey(sessionKey), decoded); var plainObj = JSON.parse(plainInfo); sessionId = plainObj.sessionid; $.cookie("sessionKey", sessionKey); $.cookie("sessionId", sessionId); // $("#content1").val("sessionid:" + sessionId + " sessionKey:" + sessionKey); resultfunc(true, "登录成功!", plainInfo); } else { resultfunc(false, retObj.msg); } }, function(info) { resultfunc(false, info); }); } else { resultfunc(false, obj.msg); } } }); } else { resultfunc(false, obj.msg); } } /* * dologin函数首先将之前请求获取到的ss值存储到sessionKey变量中, * 之后向 /login 接口发送POST请求,请求中带有username、password以及之前获取到的ps值(作为publicKey参数) * 如果请求成功,则从返回数据包中获取secretKey和info * 之后调用decryptSecretKey函数对secretKey进行解密,解密成功后操作如下: * 1. 对sessionKey值(注意是sessionKey,不是secretKey,sessionKey就是之前请求返回的ss)加密,处理后存储到浏览器cookie中 * 2. 对info(info值存储到变量cryptedInfo中,因此解密使用该变量名)进行解密,解密后获取sessionId存储到cookie中 * 3. 页面alert登录成功信息 * 另外如果dologin函数请求失败,也就是登录失败,则alert错误信息 */ 请求获得secretKey如下: 上面dologin函数解密用到的decryptSecretKey函数分析如下: // 对服务端返回的secretKey进行解密的decryptSecretKey函数 /** * 对服务端返回的secretKey进行解密,目前通过与服务端交互实现 * @param secretKey */ function decryptSecretKey(tjid, sessionkey, secretKey, succcessfunc, failfunc) { var crypted = encryptWith3DesAndEncode(sessionkey, secretKey); $.ajax({ type: "get", url: nctUrl + "/gen/unc", data: { tjid: tjid, crypted: crypted }, dataType: "json", success: function(obj) { succcessfunc(obj); }, error: function(xhr, info) { failfunc(info); } }); } /* * 该函数首先使用加密函数encryptWith3DesAndEncode,将sessionKey(此时sessionKey还未进行处理,值还是之前的服务端返回的ss的值)和secreteKey进行加密,加密后的结果赋值给crypted变量 * 之后向 /gen/unc 接口发送POST请求,请求参数是tjid(用户输入的用户名)和上面加密得到的crypted * 如果请求成功,服务端会返回一个data参数,并进行后续操作,也就是上面dologin函数分析的decryptSecretKey函数解密成功后的操作 * 如果请求失败,则alert失败信息 */ 请求解密secretKey如下: 其他加解密函数如下: /** * 用3des加密,结果采用编码返回 * @param key * @param plainText * @returns {*} */ function encryptWith3DesAndEncode(key, plainText) { if (key == null || key == '' || plainText == null || plainText == '') { return ''; } var ckey = produce3DesKey(key); var result = CryptoJS.TripleDES.encrypt(plainText, CryptoJS.enc.Utf8.parse(ckey), { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); return result.toString(); } var secretSeed = "123456789012345678901234"; function produce3DesKey(key) { if (key == undefined || key == null || key == '') return secretSeed; if (key.length >= 24) { return key.substr(0, 24); } return key + secretSeed.substr(key.length); } /** * 进行解码 * @param bs64 * @returns {*} */ function decode(bs64) { var decoded = CryptoJS.enc.Utf8.stringify(CryptoJS.enc..parse(bs64)); return decoded; } /** * 进行编码 * @param * @returns {*} */ function encode(str) { var encoded = CryptoJS.enc..stringify(CryptoJS.enc.Utf8.parse(str)); return encoded; } /** * 进行3des解密,解密之前,会将密文进行解码 * @param key * @param cryptedBs64 * @returns {*} */ function decodeAndDecryptWith3Des(key, cryptedBs64) { if (key == null || key == '' || cryptedBs64 == null || cryptedBs64 == '') { return ''; } var ckey = produce3DesKey(key); var result = CryptoJS.TripleDES.decrypt( cryptedBs64, CryptoJS.enc.Utf8.parse(ckey), { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); return result.toString(CryptoJS.enc.Utf8) } 至此登录请求过程已经分析完成,流程如下: 用户输入账号和密码,之后浏览器携带用户输入的账号向服务端/gen/key接口发起请求,如果用户输入的账号存在,则服务器返回 ss 和 ps参数浏览器携带用户输入的账号、密码以及上面请求返回的ps向服务端 /login 接口发起请求,如果信息正确,服务端返回secretKey和info参数浏览器对ss和secretKey合并加密,获取到新内容crypted,浏览器携带用户输入的账号和crypted向服务端/gen/unc接口发起请求对secretKey进行解密,解密成功后返回data参数,之后对ss进行处理,处理后的值作为sessionKey存储到浏览器cookie中,同时浏览器对之前返回的info进行解密,解密后获取到sessionId,sessionId也存储到浏览器中 下面分析第二种请求,登录后发送的普通请求登录进web系统后浏览器向服务端请求数据都是使用该请求,该请求本质是调用函数sendRequest,如下: /** * 发送请求到服务端 * @param url * @param params * @param successfunc * @param failfunc */ export function sendRequest(url, params, successfunc, failfunc, $this) { var crypted = ""; var pstr = JSON.stringify(params); var sessionKey = $.cookie("sessionKey"); var sessionId = $.cookie("sessionId"); if (pstr != "{}") { crypted = encode(encryptWith3DesAndEncode(produceSessionKey(sessionKey), pstr)); } $.ajax({ url: url, type: "post", data: crypted, dataType: "json", contentType: "application/json", xhrFields: { withCredentials: true }, crossDomain: true, beforeSend: function(xhr) { addHeaders(xhr); //发送业务请求时,需要进行签名 var timestamp = parseInt(new Date().getTime() / 1000); var random = randomString(8); var sign = produceSign(sessionId, timestamp, random, crypted); xhr.setRequestHeader("sessionid", sessionId); xhr.setRequestHeader("timestamp", timestamp); xhr.setRequestHeader("random", random); xhr.setRequestHeader("sign", sign); }, error: function(xhr, info) { failfunc(info); $this.$Message.error({ content: info, duration: 3, closable: true }) }, success: function(obj) { if (obj.code == 1) { var data = obj.data; if (data) { var decoded = decode(data); var plainText = decodeAndDecryptWith3Des(produceSessionKey(sessionKey), decoded); var plainObj = JSON.parse(plainText); successfunc(plainObj); } else { successfunc(obj); } } else { failfunc(obj); $this.$Message.error({ content: obj.msg, duration: 3, closable: true }) } } }); } /* * 该请求主要分为两部分,请求前数据加密和请求后数据解密 * 请求前数据加密: * 1. 从cookie中取出sessionKey,进行处理 * 2. 将处理后的seesionKey同要加密的数据一起通过encryptWith3DesAndEncode函数进行加密,加密后* 在进行一次编码,编码后的结果发送到服务器 * 请求后数据解密: * 1. 从cookie中取出sessionKey,进行处理 * 2. 对服务端返回的结果进行解码,之后将处理后的sessionKey和解码后的数据一同通过调用 * decodeAndDecryptWith3Des函数进行解密 */ // 对sessionKey进行处理的函数如下 function produceSessionKey(key) { return CryptoJS.MD5(APP_KEY_SEED + key).toString().toUpperCase(); } 至此登录后发送的普通请求过程分析完成,通过对以上两种类型请求过程分析,并通过使用该js文件中提供的加解密函数写出请求的加解密脚本(脚本为js脚本,可通过node调用运行),如下: 获取SeesionKey脚本, 该脚本获取的sessionKey是放置到cookie中的sessionKeyconst CryptoJS = require('crypto-js') var secretSeed = "123456789012345678901234"; function decodeAndDecryptWith3Des(key, cryptedBs64) { if (key == null || key == '' || cryptedBs64 == null || cryptedBs64 == '') { return ''; } var ckey = produce3DesKey(key); var result = CryptoJS.TripleDES.decrypt( cryptedBs64, CryptoJS.enc.Utf8.parse(ckey), { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); return result.toString(CryptoJS.enc.Utf8) } function produce3DesKey(key) { if (key == undefined || key == null || key == '') return secretSeed; if (key.length >= 24) { return key.substr(0, 24); } return key + secretSeed.substr(key.length); } var ss = ""; // 请求/gen/key接口返回的ss var data = ""; // 请求 /gen/unc 接口返回的data console.log(decodeAndDecryptWith3Des(ss, data)); 加密数据包脚本const CryptoJS = require('crypto-js') var secretSeed = "123456789012345678901234"; var APP_KEY_SEED = "terjoycht2014!@#"; /** * 用3des加密,结果采用编码返回 * @param key * @param plainText * @returns {*} */ function encryptWith3DesAndEncode(key, plainText) { if (key == null || key == '' || plainText == null || plainText == '') { return ''; } var ckey = produce3DesKey(key); var result = CryptoJS.TripleDES.encrypt(plainText, CryptoJS.enc.Utf8.parse(ckey), { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); return result.toString(); } /** * 进行编码 * @param * @returns {*} */ function encode(str) { var encoded = CryptoJS.enc..stringify(CryptoJS.enc.Utf8.parse(str)); return encoded; } function produce3DesKey(key) { if (key == undefined || key == null || key == '') return secretSeed; if (key.length >= 24) { return key.substr(0, 24); } return key + secretSeed.substr(key.length); } function produceSessionKey(key) { return CryptoJS.MD5(APP_KEY_SEED + key).toString().toUpperCase(); } var sessionKey = ''; // 浏览器cookie中的sessionKey pstr = ''; // 浏览器发送的需要加密的数据 crypted = encode(encryptWith3DesAndEncode(produceSessionKey(sessionKey), pstr)); console.log(crypted) 解密数据包脚本const CryptoJS = require('crypto-js') var APP_KEY_SEED = "terjoycht2014!@#"; var secretSeed = "123456789012345678901234"; function decodeAndDecryptWith3Des(key, cryptedBs64) { if (key == null || key == '' || cryptedBs64 == null || cryptedBs64 == '') { return ''; } var ckey = produce3DesKey(key); var result = CryptoJS.TripleDES.decrypt( cryptedBs64, CryptoJS.enc.Utf8.parse(ckey), { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); return result.toString(CryptoJS.enc.Utf8) } /** * 进行解码 * @param bs64 * @returns {*} */ function decode(bs64) { var decoded = CryptoJS.enc.Utf8.stringify(CryptoJS.enc..parse(bs64)); return decoded; } function produceSessionKey(key) { return CryptoJS.MD5(APP_KEY_SEED + key).toString().toUpperCase(); } function produce3DesKey(key) { if (key == undefined || key == null || key == '') return secretSeed; if (key.length >= 24) { return key.substr(0, 24); } return key + secretSeed.substr(key.length); } sessionKey = ""; // 存储在浏览器Cookie中的sessionKey let data = ""; // 要解密的数据,可以是请求包,也可以是服务端返回包的数据 let crypted = decodeAndDecryptWith3Des(produceSessionKey(sessionKey), decode(data)); console.log(crypted); 脚本使用对数据进行加解密如下: 获取sessionKey 至此已完成数据包的加解密,当准备进行渗透测试时,发现只要修改数据包,服务端就会返回401错误 然后问了主管,发现请求包中存在签名sign,还需要修改数据包签名,于是查看js中的签名函数,编写生成签名脚本 签名函数如下/*** * 根据给出的数据产生签名 * @param sessionid * @param timestamp * @param random * @param crypted 加密之后的请求参数 * @returns {string} */ function produceSign(sessionid, timestamp, random, crypted) { var arr = new Array(); arr.push("sessionid=" + sessionid); arr.push("apptype=" + APP_TYPE); arr.push("iversion=" + APP_IVERSION); arr.push("versioncode=" + APP_VERSION); if (crypted != "") arr.push("data=" + crypted); arr.push("timestamp=" + timestamp); arr.push("random=" + random); arr.push("secretkey=" + produceSessionKey($.cookie("sessionKey"))); arr.sort(); var signString = "[" + arr.join() + "]"; return CryptoJS.MD5(signString).toString().toUpperCase(); } 签名脚本如下const CryptoJS = require('crypto-js') var APP_KEY_SEED = "terjoycht2014!@#"; var APP_TYPE = "20"; var APP_VERSION = "388"; var APP_IVERSION = "2"; /*** * 根据给出的数据产生签名 * @param sessionid * @param timestamp * @param random * @param crypted 加密之后的请求参数 * @returns {string} */ function produceSign(sessionid, timestamp, random, crypted) { var arr = new Array(); arr.push("sessionid=" + sessionid); arr.push("apptype=" + APP_TYPE); arr.push("iversion=" + APP_IVERSION); arr.push("versioncode=" + APP_VERSION); if (crypted != "") arr.push("data=" + crypted); arr.push("timestamp=" + timestamp); arr.push("random=" + random); arr.push("secretkey=" + produceSessionKey(sessionKey)); arr.sort(); var signString = "[" + arr.join() + "]"; return CryptoJS.MD5(signString).toString().toUpperCase(); } function produceSessionKey(key) { return CryptoJS.MD5(APP_KEY_SEED + key).toString().toUpperCase(); } var sessionId = ""; // 请求头中的sessionId var sessionKey = ""; // 浏览器Cookie中的sessionKey var timestamp = ""; // 请求包中的 timestamp var random = ""; // 请求包中random var crypted = ""; // 加密后的请求数据 let sign = produceSign(sessionId, timestamp, random, crypted) console.log(sign) 但是用编写的签名脚本生成签名后,服务端依旧返回401 感觉哪里不对,于是对最初的请求进行计算签名,查看是否和请求中的签名一致 生成的签名为B1576578C9D96654E7FBF961907DD6B6, 原始请求签名为B467D7093028A1B4E9BB97492B76EF9F,两者并不一致,猜测该网站开发人员修改了签名中的部分配置参数,为了找寻修改后的配置参数,于是进行chrome debug 0x04 chrome debug 进行签名破解首先进入后台,之后打开chrome开发人员工具,在Sources菜单处,找到页面js源代码位置,因为该网站对源代码进行了打包,因此js文件分为app , mainfest 和 vendor这三类,只用查看app的js文件即可,app的js文件为该网站开发人员的开发代码打包后的文件,另外需要点击pretty-print,方便调试 之后找到一处可控的查询位置处,点击查询 抓包,获取请求接口 在js源代码中搜索该接口,因查询到该接口有4处,于是在该4处全打上断点 打上断点后,再一次点击查询,chrome debug开始 调试跟踪找到签名函数调用位置后,在签名函数的调用位置打上断点,然后点击Resume script excution 取消之前接口处的4个断点,仅留该签名函数处的断点,重新点击查询,chrome debug开始 点击箭头处,深入到函数执行内部 两个箭头搭配使用 寻找到全局参数的不同点,即开发人员将APP_TYPE 这个参数从之前的js文件中的20修改为了42 之后修改签名脚本中的该参数,调用签名脚本获取签名,并和之前请求包中的签名进行比对,结果一致,成功获取签名,改包之后也可生成新的签名,成功发包 修改后签名脚本如下: const CryptoJS = require('crypto-js') var APP_KEY_SEED = "terjoycht2014!@#"; var APP_TYPE = "42"; var APP_VERSION = "388"; var APP_IVERSION = "2"; /*** * 根据给出的数据产生签名 * @param sessionid * @param timestamp * @param random * @param crypted 加密之后的请求参数 * @returns {string} */ function produceSign(sessionid, timestamp, random, crypted) { var arr = new Array(); arr.push("sessionid=" + sessionid); arr.push("apptype=" + APP_TYPE); arr.push("iversion=" + APP_IVERSION); arr.push("versioncode=" + APP_VERSION); if (crypted != "") arr.push("data=" + crypted); arr.push("timestamp=" + timestamp); arr.push("random=" + random); arr.push("secretkey=" + produceSessionKey(sessionKey)); arr.sort(); var signString = "[" + arr.join() + "]"; return CryptoJS.MD5(signString).toString().toUpperCase(); } function produceSessionKey(key) { return CryptoJS.MD5(APP_KEY_SEED + key).toString().toUpperCase(); } var sessionId = ""; // 请求头中的sessionId var sessionKey = ""; // 浏览器Cookie中的sessionKey var timestamp = ""; // 请求包中的 timestamp var random = ""; // 请求包中random var crypted = ""; // 加密后的请求数据 let sign = produceSign(sessionId, timestamp, random, crypted) console.log(sign) 0x05 后记这里主要记录写这篇文章复盘时的一些新发现 因为sessionKey是存储在cookie中的,因此不用特意写脚本获取,可以直接在浏览器的cookie中找到sessionKey![]() ![]() 近期渗透测试基础课程有公开课,跟着大佬学挖漏洞!可留言发课程哦! *本文章仅供技术交流分享,请勿做未授权违法攻击,雨笋教育不负任何责任。具体请参考《网络安全法》。 |
CopyRight 2018-2019 实验室设备网 版权所有 |