超详细的裁判文书网反爬虫破解流程

查看 173|回复 10
作者:qystudio   
[color=]这篇文章是写给新人的。非常浅显易懂,但是不影响它包含整个破解的全部流程
[color=]想要代码的可以直接走了,本篇文章仅仅作为逆向的思路,不包含具体的爬虫代码
[color=]第一次写逆向相关的东西,各位多多包涵吧
[color=]这篇文章最初发布在我的博客上,这个是复制的,格式之类的没特别优化。论坛又不让放地址(算联系方式),故请多包涵
2022/09/07 编辑内容:按照各位管理要求,我抽时间重新上传了所有的图片,各位可以安心阅读了
前言
我在刷B站的时候又看到了那个气人的刘振智案。如果你不知道这是个什么案子,就请你自行百度。
为了更多了解这个案子,我注册了个中国裁判文书网,查询裁判文书。结果文书倒没咋看,就立刻看到了一个说法,说这个网站的反爬虫贼厉害,很难搞。真的?那我得挑战一下。
开搞
抓包
我的习惯是:任何爬虫的第一步都是抓包开搞。于是很正常地,我随机点开了一个文书,在F12->Network里面抓了一下,结果却是引入眼帘的Debugger。不过这也算常规操作,到Sources里把这个玩意儿点成蓝色的就好:


image.png (439 Bytes, 下载次数: 0)
下载附件
2022-9-7 17:06 上传

。这一步完成以后,切到Network里面,就能看到文书具体内容的请求了。筛选Fetch/XHR,看到了一个可疑的请求:


image.png (824 Bytes, 下载次数: 0)
下载附件
2022-9-7 17:07 上传

返回内容是这样的:


image.png (16.59 KB, 下载次数: 0)
下载附件
2022-9-7 17:07 上传

这不可疑谁可疑?但是一看,API倒是出来了,返回内容却是加密的。而最难受的是,不光返回内容是加密的,连参数都是加密的:


image.png (23.09 KB, 下载次数: 0)
下载附件
2022-9-7 17:07 上传

赶尽杀绝了属于是。当然,碰到问题不要慌,毕竟是来练技术的,我们先来好好分析一下,这是什么加密。
初探加密
既然能让我抓到API,那这个内容肯定是动态加载的,看源代码是没啥用的。那就转变思路,在F12调试窗口里面,找到Sources,搜索https://wenshu.court.gov.cn/website/parse/rest.q4w(是全局搜索哈,局部搜索大概率是没有的)。自然而然,扑空。后来我一想,这么搜也太绝对了,拆开搜更好些。于是又搜了搜https://wenshu.court.gov.cn/website/和rest.q4w。漫长的等待有了结果,rest.q4w搜到了一些有用的东西。


image.png (20.5 KB, 下载次数: 0)
下载附件
2022-9-7 17:07 上传

有两个文件,那就先看website.js吧。
在一个叫website.js的文件里(具体地址:https://wenshu.court.gov.cn/common/static/scripts/lawyeeui/website.js),14行左右,我看到了这样的代码:
[JavaScript] 纯文本查看 复制代码var $svc = {
    filesystem : $ctx + 'LawyeeUploadify/',
    validImg: $ctx + 'code/image',
    parserest : $ctx + 'website/parse/rest.q4w',
    updaterest : $ctx + 'website/crud/rest.q4w',
    uploadrest : $ctx + 'website/attachment/upload.q4w?',//上传附件地址
    listfilerest : $ctx + 'website/attachment/listfile.q4w?',//附件列表地址
    removefilerest : $ctx + 'website/attachment/removefile.q4w?',
    downloadrest : $ctx + 'website/systools/download.q4w?id=',
    readimgrest : $ctx + 'website/systools/readimg.q4w?id='
}
那这个$svc.parserest便是API的地址了。这也就诠释了为啥搜全部URL找不到。
当然了,为了严谨,我还是看了看index.js,结果出现在这一段:
[JavaScript] 纯文本查看 复制代码var $website = {
    "copyType" : "",
    "created" : "4a6b2814a55546279a1afbf8d1e7dfa8",
    "dataParsePath" : "/website/parse/rest.q4w",
    "department" : "",
    "description" : "",
    "domain" : "/website/",
    "enCode" : "bFVTbc2ti9IK5NtwcfmepCaB",
    "enName" : "wenshu",
    "grayscalFlag" : false,
    "group" : "c0d9f9f67e6b417ab5644f1f3b9d7fe4",
    "home" : "wenshu/181010CARHS5BS3C/index.html",
    "id" : "bb0e94898ff337e214e66a706d96ac8c",
    "isDeleted" : 0,
    "keywords" : "",
    "loggedFlag" : true,
    "name" : "裁判文书二期",
    "organization" : "9208e8585e6045058d96208d90c5dd3a",
    "prodFlag" : false,
    "siteType" : "pc",
    "statisticsFlag" : false,
    "updated" : "4a6b2814a55546279a1afbf8d1e7dfa8",
    "userName" : "超级管理员",
    "wxAppSecre" : "",
    "wxAppid" : ""
};
而局部搜索以后,整个文件就只有这一处,排除了。
那我们继续回到website.js看看。搜索$svc.parserest,只有一处,出现在220行,代码如下:
var url = $website.dataParsePath || $svc.parserest;
既然如此,那就往下找吧,果然,241行开始,有一个Ajax,用到了这个url,复制来给各位看看
[JavaScript] 纯文本查看 复制代码$.ajax({
                    url : url,
                    type : opt.type,
                    dataType : opt.dataType,
                    async : opt.async,
                    cache : opt.cache,
                    ifModified : opt.ifModified,
                    data : postData,
                    success : function(data) {
                        if (data.code == 1 || data.code == "success") {
                            if(data.secretKey){
                                var obj = DES3.decrypt(data.result, data.secretKey);
                                try{ obj = $.parseJSON(obj) }catch(e){ }
                                data.result = obj;
                            }
                            if(opt.rollback){
                                if(typeof opt.rollback === "function"){
                                    if(data.result== null && typeof data.description === "string"){
                                        try{
                                            opt.rollback($.parseJSON(data.description));
                                        }catch(e){
                                            $.WebSite.appendToView(item || "msg", e);
                                        }
                                    }else{
                                        opt.rollback(data.result);
                                    }
                                }
                            }
                        } else {
                            if(typeof opt.error === "function"){
                                opt.error(data);
                            } else {
                                // 用户未登录
                                
                                if (data.code == -4) {
                                    if(typeof loginWindow == "function"){
                                        loginWindow();
                                    } else {
                                        $.WebSite.msg({
                                            msg: "用户未登录,请重新登录",
                                            type: 2
                                        })
                                    }
                                }
                                // 访问数据过于频繁,需要验证码验证
                                
                                else if (data.code == -11) {
                                    layer.open({
                                        type: 1,
                                        area: ['420px', '240px'], //宽高
                                        title: "访问受限",
                                        closeBtn: 0,
                                        btn: ['提交验证'],
                                        yes: function(index, layero){
                                            opt.param["antitheftImageCode"] = $("input#imageCode").val();
                                            if(opt.param["antitheftImageCode"] == ""){
                                                $.WebSite.msg({
                                                    msg: "请输入验证码",
                                                    type: 2
                                                });
                                                $("input#imageCode").css({"border": "1px solid red"});
                                                return;   
                                            }
                                            $.WebSite.getData(opt);
                                            if(opt["$m"]){
                                                setTimeout(function(){
                                                    $.WebSite.invoke(opt["$m"]["randomId"], "onloadMethod");
                                                }, 500);
                                            }
                                            layer.close(index);
                                        },
                                        content:
                                            '' +
                                            '提示:' + data.description + '' +
                                            '验证码:' +
                                        '
[img][/img]
' +
                                        ''
                                    });
                                    $("body").on("click", "img#codeImagePic", function(){
                                        $.WebSite.reloadCaptcha(this);
                                    })
                                } else {
                                    /*$.WebSite.msg({
                                        msg: data.description||"数据解析异常",
                                        type: 2
                                    })*/
                                }
                            }
                        }
                    },
                    error : function(XMLHttpRequest, textStatus, errorThrown) {
                        $.WebSite.appendToView("msg", errorThrown);
                    },
                    complete: function(XMLHttpRequest){$.WebSite.loading({isShow:false});}
                });
挺长的,不过好在规范。只看前几行就能看到:var obj = DES3.decrypt(data.result, data.secretKey);
你看那个result和secretKey,不就是返回的数据吗?外面套了个3DES,这就说明,这个数据的加密算法是3DES.
二探加密
既然知道了,那就找个3DES的解密网站解密看看吧。
我找的这个:http://tool.chacuo.net/crypt3des
但是很明显,这个解不出来。因为显而易见,这里的加密模式,填充,偏移量(iv),编码都是不确定的。所以我们需要继续探寻这个3DES的加密模式、填充模式、偏移量和编码。我可不想再全局搜索了,先取巧看看局部搜索DES3能不能出来。幸运的是,在1790行,找到了相关代码。
前面的代码那么标准,到后面就乱得没眼看,不知道是程序员无心而为还是有意为之,但这绝对不是什么很好的反爬虫方式。
相关代码格式化以后可以得到这样的结果:
[JavaScript] 纯文本查看 复制代码var DES3 = {
    iv: function() {
        return $.WebSite.formatDate(new Date(), "yyyyMMdd")
    },
    encrypt: function(b, c, a) {
        if (c) {
            return (CryptoJS.TripleDES.encrypt(b, CryptoJS.enc.Utf8.parse(c), {
                    iv: CryptoJS.enc.Utf8.parse(a || DES3.iv()),
                    mode: CryptoJS.mode.CBC,
                    padding: CryptoJS.pad.Pkcs7
                }))
                .toString()
        }
        return ""
    },
    decrypt: function(b, c, a) {
        if (c) {
            return CryptoJS.enc.Utf8.stringify(CryptoJS.TripleDES.decrypt(b, CryptoJS.enc.Utf8.parse(c), {
                    iv: CryptoJS.enc.Utf8.parse(a || DES3.iv()),
                    mode: CryptoJS.mode.CBC,
                    padding: CryptoJS.pad.Pkcs7
                }))
                .toString()
        }
        return ""
    }
};
看看,这问题不就解决了?根据上述代码,即使是不懂JS和加密的人,有点英语基础就知道:
- 加密模式:CBC
- 填充模式:pkcs7(pkcs7padding)
- iv偏移量:CryptoJS.enc.Utf8.parse(a || DES3.iv()),
但聪明人都看出来了,这偏移量里面竟然还有代码,那可不行,得解出来。
首先需要搞清楚,CryptoJS.enc.Utf8.parse();是干啥的。看名字都能懂,但还是百度一下。这个API是用来从UTF8编码解析出原始字符串的,它还有个兄弟,叫CryptoJS.enc.Utf8.stringify();,用来把字符串进行UTF-8编码。所以这个可以相当于解码,删掉得了,到时候出问题了再加一个类似的解码就好了。里面的a || DES3.iv()是一个短路逻辑运算符,运算规则如下:
假若a || b 则
- 当 a == true 时,无论b是什么,都返回a
- 当 a == false 时,无论b是什么,都返回b
所以问题就是,这个a到底是不是true,换句话说,有没有值。至于a是啥,它就是个传进来的参。不慌,我们回头看看这个解密咋写的:decrypt: function(b, c, a) { ... },而在Ajax的请求中,却写的是:DES3.decrypt(data.result, data.secretKey),a呢?没传。那就当undefined吧,所以既然a不存在,那么偏移量就顺理成章成了DES3.iv(),追溯源代码,也就是$.WebSite.formatDate(new Date(), "yyyyMMdd"),也就是当前日期的yyyyMMdd格式,有点基础就能看懂,而且看看这个formatDate方法也确实如此,例如今天是2022年8月26日,那么偏移量便是20220826
所以:
- 加密模式:CBC
- 填充模式:pkcs7(pkcs7padding)
- iv偏移量:当日的yyyyMMdd格式,例如:(20220826)
现在就可以心满意足地去解密了:


image.png (60.39 KB, 下载次数: 0)
下载附件
2022-9-7 17:07 上传

解密结果不方便展示,反正你只需要知道,解密成功了
这不就搞定了吗?
结束了?
这当然没完,不还有POST参数没搞定吗。那来看看吧:docId这玩意和URL里面的docId一模一样,就是文书的唯一Id,也没加密,URL里面可以直接拿,过。ciphertext就有意思了,长得跟个二进制一样的(指不定就是二进制呢),好,那先解决你:
先取个巧,website.js里面局部搜,没搜到。那就只能去Sources里面全局搜索,搜到了一堆结果:


image.png (64.04 KB, 下载次数: 0)
下载附件
2022-9-7 17:08 上传



image.png (57.4 KB, 下载次数: 0)
下载附件
2022-9-7 17:08 上传

欸等等?.py?DangoTranslate?


image.png (52.88 KB, 下载次数: 0)
下载附件
2022-9-7 17:08 上传

程序员啊,你也不想你上班摸鱼还用了翻译器这件事被别人知道吧(划掉)
好好好,玩笑开完了,但是Desktop里的内容都能被访问到还是不安全的嗷。分析分析,index.js和strToBinary.js里面都有var ciphertext的字样,那就一个一个看看吧。在index.js里面,ciphertext定义的时候调用了cipher(),但是整个JS里面都没看到这个方法的定义,那就先放着,看看strToBinary.js吧。
针不戳,在strToBinary.js里面,第一行就是cipher这个方法的定义:
[JavaScript] 纯文本查看 复制代码function cipher() {
    var date = new Date();
    var timestamp = date.getTime().toString();
    var salt = $.WebSite.random(24);
    var year = date.getFullYear().toString();
    var month = (date.getMonth() + 1
啊?又是Date?你们就这么喜欢日期吗?(划掉)
既然来了,那就面对它,读读看看,反正不复杂。
上面的ciphertext里面又调用了strTobinary(),反正cipher()这个方法后面就是它,搬出来看看吧
[JavaScript] 纯文本查看 复制代码function strTobinary(str) {
    var result = [];
    var list = str.split("");
    for (var i = 0; i
timestamp一看就是时间戳:


image.png (2.52 KB, 下载次数: 0)
下载附件
2022-9-7 17:08 上传

接下来又遇到麻烦了,$.WebSite.random(24)是个什么东西?


image.png (15.89 KB, 下载次数: 0)
下载附件
2022-9-7 17:08 上传

这么看,这就是取长度为24的字符串,那就看看它的算法吧。既然是$.WebSite,那我猜它就在那个万恶的website.js里面。一搜,果不其然。在1006行,找到了代码
[JavaScript] 纯文本查看 复制代码random: function(size){
            var str = "",
            arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
            for(var i=0; i
算法很简单,就是先定义一个空字符串str作为未来的返回值,然后有一个从0-9,到a-z到A-Z的数组作为字典,再用一个for循环,循环size次,把str的内容变成自身再加上arr字典里面的一个随机下标,这个随机下标就是一个随机数乘上这个字典的长度-1再四舍五入。这里解释一下:
- Math.round();是四舍五入
- Math.random();是随机数
从这里我们也能看出来一件事:这里Math.random();的返回值是不确定的,这也就意味着整个随机字符串的值也是不固定且无法预测的。所以,这个算法我们也可以不用,自己造一个照理说也能通过(我觉得哈)
再回到cipher();方法,毕竟上面都是支线任务,主线任务还没搞定呢。
year取的就是年份,这里返回的是2022,很简单,不管它。而month和day则用到了三元运算符。我来讲讲:
三元运算符的语法:a ? b : c
a是逻辑表达式,比如 1 > 2 返回false这般,当a返回true是,整体返回b,反之返回c。
而当你了解了三元运算符以后,上述代码就变得十分清晰了,我就不赘述了。为了省得各位往上翻,我把后面的代码复制过来:
[JavaScript] 纯文本查看 复制代码var iv = year + month + day;
var enc = DES3.encrypt(timestamp, salt, iv).toString();
var str = salt + iv + enc;
var ciphertext = strTobinary(str);
return ciphertext;
这里就能看到,它又用到了3DES(万恶的website.js)。最终的ciphertext的值对于各位来说就很简单了,整个算法也并不难。
至于那个strTobinary();呢,是固定算法,也不难,想复刻也很简单,我就不赘述了。
最后一击
那个cfg虽然复杂,但好歹能看懂,也应当是个固定值,略过。
最后一击,就是攻破这个__RequestVerificationToken。再次全局搜索。


image.png (32.07 KB, 下载次数: 0)
下载附件
2022-9-7 17:09 上传

怎么又是你?website?
关键性的代码是这一行:$("body").append('');
好了,破案了,又是这个random。
最终分析
所以,发请求的时候,参数就这么搞:
- docId: 从URL里面扣,很好搞
- ciphertext: 根据上述算法获取
- cfg: com.lawyee.judge.dc.parse.dto.SearchDataDsoDTO@docInfoSearch
- __RequestVerificationToken: 固定采集
剩下的几个参数我就没管了,有兴趣的可以自己看看,应当不难
所以,我们只需要经常更新Cookie还有Ciphertext,剩下的都是定值,定期采集一下固定住就好了
后话
没想到这东西的反爬虫这么麻烦,是我轻敌了。做完以后感觉好有成就感啊~
本篇只是个思路哈,不提供实质性的代码。这么详细的思路都有了写个爬虫还难吗(划掉),当然啦,那要是真想爬,做好吃免费饭的心理准备。
另外,我发现有人卖这个东西的数据库,还TM是按量卖的,太黑了,会个爬虫就这么嚣张。
最后,送各位这么几句话:
一、本裁判文书库公布的裁判文书由相关法院录入和审核,并依据法律与审判公开的原则予以公开。若有关当事人对相关信息内容有异议的,可向公布法院书面申请更正或者下镜。
二、本裁判文书库提供的信息仅供查询人参考,内容以正式文本为准。非法使用裁判文书库信息给他人造成损害的,由非法使用人承担法律责任。
三、本裁判文书库信息查询免费,严禁任何单位和个人利用本裁判文书库信息牟取非法利益。
四、未经允许,任何商业性网站不得建立本裁判文书库的镜像(包括全部和局部镜像)。
五、根据有关法律规定,相关法院依法定程序撤回在本网站公开的裁判文书的,其余网站有义务免费及时撤回相应文书。
(以上内容直接复制自国家裁判文书网)

文书, 代码

z1142257131   

上传一下图片吧,首先博客图床不稳定,上传到论坛可以保活
此外就是你论坛没有ssl啊喂。。。
图片全都炸了
(小声逼逼看你博客里用的md。。。为什么发帖不用md)
梦回凉亭的她   


jiahy 发表于 2022-12-6 00:26
怎么样,我这也是,已经封了我2000多个了

现在只要请求rest.q4w基本都全封了,不管你请求多少,一条也封。但selenium少量请求现在还行,起码账号不会被封。页面上没感觉有限制,应该是后台设置的
梦回凉亭的她   

这个网站以前是瑞数,所以难,现在估计到期了没续费
wantwill   

想练手的话可以用商标查询网,那个经典
隔壁家的王二狗   

楼主你辛苦了吧
bluety123   

图片全在转圈圈
zhangsf123   

受教了  收藏学习一波
小黑屋   

我当初也想爬一下试一试。结果看了那么多加密有点头疼。
最后我开了个内存chrome,直接开网页。
Wangxianhao   

这个站又可以爬了
您需要登录后才可以回帖 登录 | 立即注册