diff --git a/integration/auth_key_test.go b/integration/auth_key_test.go index 90034434..0049a810 100644 --- a/integration/auth_key_test.go +++ b/integration/auth_key_test.go @@ -451,3 +451,83 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) { }) } } + +func TestDuplicateNodeKeysSpoofing2731(t *testing.T) { + IntegrationSkip(t) + + spec := ScenarioSpec{ + NodesPerUser: 0, + Users: []string{}, + } + + scenario, err := NewScenario(spec) + assertNoErr(t, err) + // defer scenario.ShutdownAssertNoPanics(t) + + err = scenario.CreateHeadscaleEnvWithLoginURL( + nil, + hsic.WithTestName("weblogout"), + hsic.WithTLS(), + ) + assertNoErrHeadscaleEnv(t, err) + + err = scenario.WaitForTailscaleSync() + assertNoErrSync(t, err) + + hs, err := scenario.Headscale() + assertNoErrGetHeadscale(t, err) + + adminUser, err := scenario.CreateUser("admin") + require.NoError(t, err) + + attackerUser, err := scenario.CreateUser("attacker") + require.NoError(t, err) + + adminNode, err := scenario.CreateTailscaleNode("unstable", tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork])) + require.NoError(t, err) + + attackerNode, err := scenario.CreateTailscaleNode("unstable", tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork])) + require.NoError(t, err) + + adminPAK, err := scenario.CreatePreAuthKey(adminUser.Id, true, false) + require.NoError(t, err) + + attackerPAK, err := scenario.CreatePreAuthKey(attackerUser.Id, true, false) + require.NoError(t, err) + + err = adminNode.Login(hs.GetEndpoint(), adminPAK.GetKey()) + require.NoError(t, err) + + err = attackerNode.Login(hs.GetEndpoint(), attackerPAK.GetKey()) + require.NoError(t, err) + + require.EventuallyWithT(t, func(ct *assert.CollectT) { + nodes, err := hs.ListNodes() + assert.NoError(ct, err) + assert.Len(ct, nodes, 2, "there should be two nodes registered") + + users, err := hs.ListUsers() + assert.NoError(ct, err) + assert.Len(ct, users, 2, "there should be two users created") + }, 30*time.Second, 1*time.Second, "ensuring nodes and users are created") + + nodePriv, err := adminNode.GetNodePrivateKey() + require.NoError(t, err) + + err = attackerNode.SetNodePrivateKey(*nodePriv) + require.NoError(t, err) + + err = attackerNode.ReloadTailscaled() + require.NoError(t, err) + + nodePriv2, err := attackerNode.GetNodePrivateKey() + require.NoError(t, err) + + require.Equal(t, nodePriv.Public().String(), nodePriv2.Public().String(), "node private keys should be equal") + + // require.NoError(t, attackerNode.Logout()) + // require.NoError(t, attackerNode.WaitForNeedsLogin()) + + err = attackerNode.Login(hs.GetEndpoint(), attackerPAK.GetKey()) + require.NoError(t, err) +} diff --git a/integration/tailscale.go b/integration/tailscale.go index 07573e6f..374f3b39 100644 --- a/integration/tailscale.go +++ b/integration/tailscale.go @@ -41,6 +41,8 @@ type TailscaleClient interface { Netmap() (*netmap.NetworkMap, error) DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error) GetNodePrivateKey() (*key.NodePrivate, error) + SetNodePrivateKey(privateKey key.NodePrivate) error + ReloadTailscaled() error Netcheck() (*netcheck.Report, error) WaitForNeedsLogin(timeout time.Duration) error WaitForRunning(timeout time.Duration) error diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index 665fd670..aa737d5d 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -1333,3 +1333,69 @@ func (t *TailscaleInContainer) GetNodePrivateKey() (*key.NodePrivate, error) { return &p.Persist.PrivateNodeKey, nil } + +func (t *TailscaleInContainer) SetNodePrivateKey(privateKey key.NodePrivate) error { + state, err := t.ReadFile(paths.DefaultTailscaledStateFile()) + if err != nil { + return fmt.Errorf("failed to read state file: %w", err) + } + store := &mem.Store{} + if err = store.LoadFromJSON(state); err != nil { + return fmt.Errorf("failed to unmarshal state file: %w", err) + } + + currentProfileKey, err := store.ReadState(ipn.CurrentProfileStateKey) + if err != nil { + return fmt.Errorf("failed to read current profile state key: %w", err) + } + currentProfile, err := store.ReadState(ipn.StateKey(currentProfileKey)) + if err != nil { + return fmt.Errorf("failed to read current profile state: %w", err) + } + + p := &ipn.Prefs{} + if err = json.Unmarshal(currentProfile, &p); err != nil { + return fmt.Errorf("failed to unmarshal current profile state: %w", err) + } + + // Update the private node key + p.Persist.PrivateNodeKey = privateKey + + // Marshal the updated preferences back to JSON + updatedProfile, err := json.Marshal(p) + if err != nil { + return fmt.Errorf("failed to marshal updated profile state: %w", err) + } + + // Write the updated profile back to the store + if err = store.WriteState(ipn.StateKey(currentProfileKey), updatedProfile); err != nil { + return fmt.Errorf("failed to write updated profile state: %w", err) + } + + // Save the updated store back to the state file + updatedState, err := store.ExportToJSON() + if err != nil { + return fmt.Errorf("failed to export updated state: %w", err) + } + + if err = t.WriteFile(paths.DefaultTailscaledStateFile(), updatedState); err != nil { + return fmt.Errorf("failed to write updated state file: %w", err) + } + + return nil +} + +func (t *TailscaleInContainer) ReloadTailscaled() error { + command := []string{ + "kill", + "-HUP", + "1", + } + + _, _, err := t.Execute(command) + if err != nil { + return fmt.Errorf("failed to reload tailscaled: %w", err) + } + + return nil +}