If hash functions append the length, why does length extension attack work?
Let hash be the raw hash function, as you're referring to. You mentioned that the attacker knows hash(message || length), but to be more precise, they know hash(message || padding || length). Let full_hash be the proper hash with padding and length, i.e. full_hash(message) = hash(message || padding || length).

You're correct that if the attacker knows hash(message || padding || length), then they can't compute hash(message || appended_data).

But they can compute hash(message || padding || length || appended_data || actual_padding || actual_length) which is equal to full_hash(message || padding || length || appended_data), which may be enough for an attack. The inner padding and length become "garbage" which can be ignored depending on the attack scenario.

In order to carry out the attack, start from hash(message || padding || length) (i.e. the original hash), use it as the initial state for the hash, and then feed the remaining data (appended_data || actual_padding || actual_length).