samuong / alpaca

A local HTTP proxy for command-line tools. Supports PAC scripts and NTLM authentication.
Apache License 2.0
184 stars 31 forks source link

issue relating to cross-domain authentication on ntlm #125

Closed ziyiwang closed 1 week ago

ziyiwang commented 3 weeks ago

heya Sam, i found another issue

when user is connecting to a different domain proxy (i.e, testau domain) and using existing domain (au) credentials, due to the initial negotiate coming bck is testau (the proxy being in that domain ), the subsequent authenticate message sent back to that proxy is always testau , hence failing authenticaitons, this is even with user entering the right domain i.e. au with -d option.

this cross-domain auth is needed as some of our test proxies support both prod and non-prod crednetails and multi domains, many proxies do that, i..e prod, non-prod, dev , staging etc.

hence the feature needs to be added to use the user provided domain for authentication regardless of the domain that comes with the hostname of the target proxy. Curl for example handles this quite seamlessly.

i have hacked a fix with overriding ntlmssp lib which is upstream , but i think a more elegant solution could help

overrideing this method with additional user domain issue seems to solve problem , here domain_uaa is the user entered domain at launch of alpaca.

` func (m *challengeMessage) UnmarshalBinary(data []byte, domain_uaa string) error { r := bytes.NewReader(data) err := binary.Read(r, binary.LittleEndian, &m.challengeMessageFields) if err != nil { return err } if !m.challengeMessageFields.IsValid() { return fmt.Errorf("Message is not a valid challenge message: %+v", m.challengeMessageFields.messageHeader) }

if m.challengeMessageFields.TargetName.Len > 0 {
    //m.TargetName, err = m.challengeMessageFields.TargetName.ReadStringFrom(data, m.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEUNICODE))
    m.TargetName = domain_uaa
//  if err != nil {
//      return err
//  }
}

if m.challengeMessageFields.TargetInfo.Len > 0 {
    d, err := m.challengeMessageFields.TargetInfo.ReadFrom(data)
    m.TargetInfoRaw = d
    if err != nil {
        return err
    }
    m.TargetInfo = make(map[avID][]byte)
    r := bytes.NewReader(d)
    for {
        var id avID
        var l uint16
        err = binary.Read(r, binary.LittleEndian, &id)
        if err != nil {
            return err
        }
        if id == avIDMsvAvEOL {
            break
        }

        err = binary.Read(r, binary.LittleEndian, &l)
        if err != nil {
            return err
        }
        value := make([]byte, l)
        n, err := r.Read(value)
        if err != nil {
            return err
        }
        if n != int(l) {
            return fmt.Errorf("Expected to read %d bytes, got only %d", l, n)
        }
        m.TargetInfo[id] = value
    }
}

return nil

} `

samuong commented 3 weeks ago

Hi Ziyi, thanks for the bug report! I just wanted to make sure I'm understanding correctly. Here's what happens when Alpaca does the NTLM handshake:

  1. Alpaca sends a type 1 "negotiate" message to the proxy in the request. This message contains an (optional) domain, which Alpaca does send.
  2. The proxy sends back a type 2 "challenge" message in the response. This message contains an optional "target info" object, which contains the domain. Alpaca does not bother to look at this, and just assumes that the domain matches.
  3. Alpaca sends a type 3 "authenticate" message in the next request, which uses the credentials for the domain from the type 1 message.
  4. The proxy makes the request to the origin server and sends the response to Alpaca.

In your situation, it sounds like you've got two domains "au" and "testau". A user runs Alpaca with something like alpaca -d au, so the type negotiate message contains "au" and the challenge message contains "testau", since the proxy is in the "testau" domain. In this case, should the user authenticate using their "au" domain credentials, or their "testau" domain credentials?

I think you're saying that your user needs to use their "au" credentials, but the go-ntlmssp package blindly copies over the TargetName and TargetInfo from the challenge message into the authenticate message, which results in the proxy rejecting the request. And so the fix is to make sure that the authenticate message has the "au" domain in it. Is that correct?

I think you're not saying that Alpaca needs to accept multiple credentials (maybe something like alpaca -d au,testau -u user,testuser) , and have it look up the correct set of credentials depending on the TargetInfo in the challenge message. But please let me know if I've misunderstood.

ziyiwang commented 2 weeks ago

hi sam, yes, dot point 3 is the issue, it always uses the domain from the hostname of the target proxy, hence causing auth errors regardless of what domain user puts in via -d option currently

samuong commented 2 weeks ago

Ok, I see what you mean. I've built a small test server that can reproduce the curl behaviour that you're talking about, and I can see that the go-ntlmssp package that we're using doesn't match curl's behaviour. It looks like curl is the one behaving correctly here.

As you mentioned, a fix should be applied in the upstream package, not in Alpaca itself. I think it would be best to set the user's domain while constructing the authenticate message, rather than overwriting the target domain in the challenge message. But I'm not sure how to do this without breaking the public API of that module, so for the moment I'll point Alpaca at a personal fork with the fix.

I'd really appreciate if you could test it once again, before I merge and tag the next release. Any chance you can give #126 a go?

ziyiwang commented 1 week ago

hi mate , sorry in delay getting back to you, i have tested the build , it didnt inject the right domain still looksl ike, i will take a closer look over weekend, and compare it with my local fix version , i think the UnmarshalBinary needs tobe updated

ziyiwang commented 1 week ago

Ok, I see what you mean. I've built a small test server that can reproduce the curl behaviour that you're talking about, and I can see that the go-ntlmssp package that we're using doesn't match curl's behaviour. It looks like curl is the one behaving correctly here.

As you mentioned, a fix should be applied in the upstream package, not in Alpaca itself. I think it would be best to set the user's domain while constructing the authenticate message, rather than overwriting the target domain in the challenge message. But I'm not sure how to do this without breaking the public API of that module, so for the moment I'll point Alpaca at a personal fork with the fix.

I'd really appreciate if you could test it once again, before I merge and tag the next release. Any chance you can give #126 a go?

hi mate, its all good ,tested again now, it was working fine, sorry it was credential issue on our side previously. no need for change on UnmarshalBinary. pls release 2.0.5 when you can , keen to get rid of the local fix build version we using now. and just use public 2.0.5 version, thanks mate