I got exactly the same behavior with all the IAM rights, and lost a bit of time before I got it to work.
If your lambda function runs inside a VPC you have to create an endpoint for S3 as describe in this article from AWS blog.
If you want to see more in details where it hangs, you can use the following code. Instead of giving a callback a keep the reference on the request and basically observe its events (see the documentation of S3.putObject and AWS.Request).
var obj = s3.putObject(params);
obj.on('validate', (...args)=>{args.unshift('validate'); console.log(...args);})
.on('build', (...args)=>{args.unshift('build'); console.log(...args);})
.on('sign', (...args)=>{args.unshift('sign'); console.log(...args);})
.on('send', (...args)=>{args.unshift('send'); console.log(...args);})
.on('retry', (...args)=>{args.unshift('retry'); console.log(...args);})
.on('extractError', (...args)=>{args.unshift('extractError'); console.log(...args);})
.on('extractData', (...args)=>{args.unshift('extractData'); console.log(...args);})
.on('success', (...args)=>{args.unshift('success'); console.log(...args);})
.on('error', (...args)=>{args.unshift('error'); console.log(...args);})
.on('complete', (...args)=>{args.unshift('complete'); console.log(...args);})
.on('httpHeaders', (...args)=>{args.unshift('httpHeaders'); console.log(...args);})
.on('httpData', (...args)=>{args.unshift('httpData'); console.log(...args);})
.on('httpUploadProgress', (...args)=>{args.unshift('httpUploadProgress'); console.log(...args);})
.on('httpDownloadProgress', (...args)=>{args.unshift('httpDownloadProgress'); console.log(...args);})
.on('httpError', (...args)=>{args.unshift('httpError'); console.log(...args);})
.on('httpDone', (...args)=>{args.unshift('httpDone'); console.log(...args);})
.send();
By doing so I got to see the underlying HTTP request was trying to reach the public urls of the bucket, which is not possible from a VPC unless you have the endpoint :).
Here is also another post about accessing AWS ressources from a VPC also from AWS blog.