The usual method would be to use
DontDestroyOnLoad on something like a manager or "bookkeeping" object - something that's invisible to the player, but keeps track of important state information so it can be carried from scene to scene.
If you do this, you'll want to be careful to ensure this object is always available, even if you jump straight into testing scene 5 without going through the previous scenes, and that you don't build up unwanted copies of the object. Some ways to do this are:
Put the manager object in every scene, and have it destroy itself on load if a copy has been carried over from a previous scene. This gets messy and error-prone though.
Have the manager object create itself the first time it's needed, using something like a Singleton pattern with a static
GetInstance() method to return the current instance, or create one if it's missing.
Write an Editor script that adds the manager object to the scene when you hit play, or redirects the editor to load an initialization scene (containing your manager objects) and then load the scene you want to test.
This can also be done with a static class, which handles keeping the data always accessible and unique, but it can do so too well in a sense:
the data is always accessible, even if you don't need it in the current game state - the static members keep occupying space in memory whether you want them there or not. (While a manager object on the other hand can always be deleted/destroyed once you're done with it)
the data is unique, even if you want to duplicate the data in order to add multiplayer or parallel universes or other mechanics. (While on the other hand you could have multiple copies of a manager object, one for each player/universe, so long as each piece of gameplay knows which manager to use)
So, providing this feature structurally via a static class makes it simple to set up, but changing the behaviour down the line can mean big structural changes.
Either of these methods may require some re-wiring once you load back to a previous scene. From Unity's perspective, the GameObjects in the scene are all newly-created, and have no relationship to their previous incarnations - even their ID values will be different. How exactly you solve this will depend on the needs of your scene, and may be worth a detailed question of its own.
We can partly farm out the saving responsibility to the individual objects whose state needs to be saved, though you still need a central manager object or static member to store the data. To do this you'd generate a unique ID on each object that needs saving, so it can identify data from its past lives. On quitting a level, the object would submit its data to the manager to be saved, indexed by its unique ID. On loading, it would ask the manager if data already exists for its ID, and override its initial state with the manager's response if so. This simplifies the rewiring logic, since all we need is that the saveable objects can find the manager.
Lastly, if the data you're saving needs to be persistent between runs of the game, you can use PlayerPrefs. Just note that this data is stored in plaintext, so don't put anything there that you don't want the player peeking at or modifying outside the game.
You can also serialize this data to a custom file format yourself, but in that case you'll still want a manager object to hold the current state in memory & handle serialization, so you don't have to read & write a file every time a script uses this state.