autoreg-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
# GitHub Signup — Technical Bypass Encyclopedia
|
||||
|
||||
Captured 2026-07-29 from a successful registration. These are concrete techniques, not permission to mass-register.
|
||||
|
||||
## GitHub's Defense Layers (3 levels)
|
||||
|
||||
1. **DataDome JS Challenge** — blocks entire `/signup` page via JS redirect to `geo.captcha-delivery.com` or `octocaptcha.com/datadome`
|
||||
2. **Google OAuth Redirect** — if email domain is Google-linked (t-online.de, gmail.com, etc.), GitHub redirects to `/sessions/social/google/initiate` instead of showing password form
|
||||
3. **OctoCaptcha (DataDome in iframe)** — on the password form, an invisible iframe at `octocaptcha.com/datadome?origin_page=github_signup_redesign` must validate before submit button appears
|
||||
|
||||
## Layer 1: DataDome JS Challenge
|
||||
|
||||
**Symptoms:** `captcha-delivery.com` in HTML, page blocked with JS challenge.
|
||||
|
||||
**Solution:** Non-headless Chromium through SOCKS5 proxy.
|
||||
|
||||
- `headless=False` mandatory — DataDome detects headless modes via canvas/WebGL/audio fingerprinting
|
||||
- SOCKS5 proxy (NOT HTTP) — proxyshard.com HTTP proxies return 403; SOCKS5 (port 1080) works
|
||||
- Playwright does NOT support SOCKS5 with auth → need in-process bridge
|
||||
|
||||
### In-process SOCKS5 bridge (for Playwright)
|
||||
|
||||
```python
|
||||
import asyncio, struct, random
|
||||
|
||||
class SocksBridge:
|
||||
def __init__(self, host, port, user, password):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.user = user.encode()
|
||||
self.passwd = [REDACTED])
|
||||
self.lp = random.randint(12000, 20000)
|
||||
self.srv = None
|
||||
|
||||
async def _connect(self, target_host, target_port):
|
||||
r, w = await asyncio.open_connection(self.host, self.port)
|
||||
w.write(b'\x05\x02\x00\x02'); await w.drain()
|
||||
await r.readexactly(2)
|
||||
w.write(b'\x01' + bytes([len(self.user)]) + self.user + bytes([len(self.passwd)]) + self.passwd)
|
||||
await w.drain(); await r.readexactly(2)
|
||||
hb = target_host.encode()
|
||||
w.write(b'\x05\x01\x00\x03' + bytes([len(hb)]) + hb + struct.pack('>H', target_port))
|
||||
await w.drain(); await r.readexactly(10)
|
||||
return r, w
|
||||
|
||||
async def _handle(self, cr, cw):
|
||||
try:
|
||||
req = await cr.readuntil(b'\r\n\r\n')
|
||||
t = req.decode().split()[1]
|
||||
host, ps = t.split(':') if ':' in t else (t, '443')
|
||||
rr, rw = await self._connect(host, int(ps))
|
||||
cw.write(b'HTTP/1.0 200 Connection Established\r\n\r\n'); await cw.drain()
|
||||
async def rel(r1, w1):
|
||||
try:
|
||||
while True:
|
||||
d = await r1.read(65536)
|
||||
if not d: break
|
||||
w1.write(d); await w1.drain()
|
||||
except: pass
|
||||
await asyncio.gather(rel(cr, rw), rel(rr, cw))
|
||||
except: pass
|
||||
finally:
|
||||
try: cw.close()
|
||||
except: pass
|
||||
|
||||
async def start(self):
|
||||
self.srv = await asyncio.start_server(self._handle, '127.0.0.1', self.lp)
|
||||
return self.lp
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return f"http://127.0.0.1:{self.lp}"
|
||||
|
||||
# Usage:
|
||||
browser = await p.chromium.launch(
|
||||
headless=False,
|
||||
proxy={'server': bridge.url},
|
||||
args=['--no-sandbox', '--disable-blink-features=AutomationControlled']
|
||||
)
|
||||
```
|
||||
|
||||
**Detection:** After `goto /signup`, wait for email input to appear:
|
||||
```python
|
||||
for i in range(20):
|
||||
await asyncio.sleep(1)
|
||||
try:
|
||||
inp = await page.query_selector('input[name="user[email]"]')
|
||||
if inp: break
|
||||
except: continue # Execution context destroyed = page still navigating
|
||||
```
|
||||
|
||||
DataDome solves in 3-15 seconds in non-headless Chrome through SOCKS5.
|
||||
|
||||
## Layer 2: Google OAuth Redirect
|
||||
|
||||
**Symptoms:** After clicking Continue on email, redirects to `accounts.google.com` or `/sessions/social/google/initiate`.
|
||||
|
||||
**Solution:** `page.route()` with `fulfill(status=204)` — MUST use fulfill, NOT abort.
|
||||
|
||||
```python
|
||||
async def block_google(route):
|
||||
await route.fulfill(status=204, body=b"")
|
||||
|
||||
await page.route("**/sessions/social/google/**", block_google)
|
||||
await page.route("**/accounts.google.com/**", block_google)
|
||||
```
|
||||
|
||||
`route.abort()` causes `chrome-error://chromewebdata/` — unusable. `route.fulfill(204)` returns empty response, GitHub's JS falls back to password form.
|
||||
|
||||
**Email domain workaround:** t-online.de triggers Google OAuth. Use non-Google domains:
|
||||
- `proton.me` ✓
|
||||
- `mail.ru` ✓
|
||||
- `yandex.com` ✓
|
||||
- `outlook.com` ✓
|
||||
|
||||
## Layer 3: OctoCaptcha (DataDome in iframe)
|
||||
|
||||
**Symptoms:** Password form loads, but submit button never appears. `octocaptcha-token` hidden field exists but stays empty. Iframe at `octocaptcha.com/datadome` is `visible=False`.
|
||||
|
||||
**Console evidence:** `Failed to load resource: the server responded with a status of 403` from octocaptcha.com.
|
||||
|
||||
**CRITICAL FINDING — `fetch()` POST bypasses OctoCaptcha entirely:**
|
||||
|
||||
The OctoCaptcha validation happens client-side (iframe JS). Submitting the form via `fetch()` from the SAME browser context with valid cookies + authenticity_token bypasses the captcha check. GitHub's server accepts the form without captcha when all other signals (cookies, CSRF token, browser fingerprint) are correct.
|
||||
|
||||
```javascript
|
||||
// Run in page.evaluate() after filling all form fields
|
||||
const formData = new URLSearchParams();
|
||||
document.querySelectorAll('input').forEach(inp => {
|
||||
if (inp.name && inp.name !== 'octocaptcha-token' && !inp.name.startsWith('required_field_')) {
|
||||
formData.append(inp.name, inp.value || '');
|
||||
}
|
||||
});
|
||||
// Override with our values
|
||||
formData.set('user[email]', email);
|
||||
formData.set('user[password]', password);
|
||||
formData.set('user[login]', username);
|
||||
|
||||
const resp = await fetch('/signup', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: formData.toString(),
|
||||
redirect: 'follow'
|
||||
});
|
||||
// Success: resp.url === 'https://github.com/account_verifications'
|
||||
```
|
||||
|
||||
**Key form fields:**
|
||||
- `authenticity_token` — CSRF token (extract from page HTML)
|
||||
- `user[email]` — email
|
||||
- `user[password]` — password
|
||||
- `user[login]` — username
|
||||
- `timestamp` + `timestamp_secret` — anti-replay (extract from page)
|
||||
- `user_signup[copilot_opt_in]` — set to '0'
|
||||
- `user_signup[marketing_consent]` — set to '0'
|
||||
- `octocaptcha-token` — SKIP (not needed with fetch)
|
||||
- `required_field_*` — HONEYPOT (must be empty/not sent)
|
||||
|
||||
## Proxy Rotation
|
||||
|
||||
proxyshard.com German residential proxies use SID for IP rotation:
|
||||
```
|
||||
Format: plan-premium-country-de-sid-{SID}-ttl-8000000
|
||||
SOCKS5: relay-eu.proxyshard.com:1080
|
||||
Password: [REDACTED]
|
||||
```
|
||||
|
||||
Each different SID = different exit IP. SIDs from `proxyshard_de.txt` work on both HTTP (8080) and SOCKS5 (1080) ports. SOCKS5 is required for GitHub — HTTP gets 403 from DataDome.
|
||||
|
||||
Rate limit: after ~6 signup attempts, GitHub returns "Too many requests · GitHub" on the same IP. Rotate SID.
|
||||
|
||||
## Full Working Flow
|
||||
|
||||
1. Start in-process SOCKS5 bridge with fresh SID
|
||||
2. Launch Playwright `headless=False` through bridge
|
||||
3. Set up Google OAuth route blocking
|
||||
4. `goto /signup` → wait for email input (DataDome auto-solves in non-headless Chrome)
|
||||
5. Fill email → click Continue → blocked Google redirect → password form appears
|
||||
6. Fill password + username + extract authenticity_token/timestamp_secret
|
||||
7. `page.evaluate()` → `fetch('/signup', {method:'POST', body:formData})`
|
||||
8. Check `response.url` for `account_verifications` → success
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **page.content() during navigation crashes** — "Execution context was destroyed". Always wait for navigation to settle, or use try/except with retry.
|
||||
- **OctoCaptcha iframe `visible=False`** — by design. Don't try to make it visible. Use fetch() instead.
|
||||
- **`route.abort()` is WRONG** for Google redirect — causes chrome-error page. Use `route.fulfill(status=204)`.
|
||||
- **Honeypot fields** (`required_field_*`) — leave empty. GitHub's anti-bot checks if they're filled.
|
||||
- **Button click "element is not enabled"** — Playwright waits for stability, but form submission navigates away. Use JS click or fetch().
|
||||
- **Bridge port conflicts** — always kill old Chrome processes before restarting bridge.
|
||||
- **mail.tm disposable domains silently blocked** — OTP form accepts the code, GitHub redirects to home page, but the account never exists (404). Use `t-online.de` (Telekom Germany) or other non-disposable domains. See `references/github-autoreg-email-domain-blocklist.md`.
|
||||
- **Camoufox OTP submit** — use `form.requestSubmit()` not `form.submit()` for the launch-code form. Playwright `btn.click()` fails on disabled button. Fill each `launch_code[]` box individually, then JS-submit via `page.evaluate`.
|
||||
- **Python 3.11 + hermes venv isolation** — `py -3.11` can pick up hermes venv's playwright, causing `PermissionError`. Use `PYTHONPATH=` or `-I` flag.
|
||||
- **`pending_login` ≠ account exists** — after CDP OTP flow, `pending_login` status means the OTP was accepted but the session wasn't confirmed. Always verify with GitHub API (`GET /users/{username}`) before assuming the account was created.
|
||||
- **Camoufox proxy format — `geoip=True` required**: When using a proxy with Camoufox, ALWAYS pass `geoip=True` in launch_opts. Without it, Camoufox emits a `LeakWarning`. The proxy config must use `playwright_proxy_config()` from `core/proxy_utils.py` to split the URL into `{server, username, password}` — embedding credentials in the `server` field causes `NS_ERROR_PROXY_CONNECTION_REFUSED`.
|
||||
- **bpproxy.at pool dead (2026-08-10)**: All 10+ proxies from the hardsession pool return empty HTTP responses. curl exits 0 with no body. Use ZTE mobile proxy instead — see `references/github-autoreg-email-domain-blocklist.md`.
|
||||
- **DataDome captcha solver compatibility (2026-08-12)**: Only **2Captcha** supports DataDome. `yescaptcha_api` and `capsolver_api` return `不支持 DataDome`. 2Captcha requires positive balance (`ERROR_ZERO_BALANCE` otherwise). If 2Captcha balance is zero, DataDome pre-solve returns empty cookie and registration fails.
|
||||
- **ENI AUTOREG task config — `use_proxy` field**: To use a custom proxy (e.g. ZTE mobile), `use_proxy:"0"` MUST be in the `extra` sub-dict, NOT the top-level config. The task runtime reads `extra.get("use_proxy")` to decide whether to use the proxy pool. If omitted, the pool's round-robin proxies override the custom `proxy` URL. Correct format: `{"config":{"executor_type":"headed","proxy":"http://127.0.0.1:8899","extra":{"use_proxy":"0"}}}`.
|
||||
- **IMAP pool config wipe via PUT API**: When updating a provider via `PUT /api/providers/mailbox/{key}`, the `config` dict REPLACES the existing config entirely. If `"config":{}` is sent, all existing fields (`imap_pool_file`, `imap_host`, etc.) are wiped. Always include the full config dict when updating.
|
||||
Reference in New Issue
Block a user