How to use NSTask as root? - cocoa

In an application I'm making I need to run the following command as root (user will be prompted trice if they really want to, and they will be asked to unmount their drives) using NSTask:
/bin/rm -rf /
#Yes, really
The problem is that simply using Substitute User Do (sudo) doesn't work as the user needs to enter the password to the non-available stdin. I'd rather like to show the user the same window as you'd see when you click the lock in Preferences.app, like this (hopefully with a shorter password):
(source: quickpwn.com)
Can anyone help me with this? Thanks.

Check out STPrivilegedTask, an Objective-C wrapper class around AuthorizationExecuteWithPrivileges() with an NSTask-like interface.

That's one of the hardest tasks to do properly on Mac OS X.
The guide documenting how to do this is the Authorization Services Programming Guide. There are multiple possibilities, as usual the most secure is the hardest to implement.
I've started writing a tool that uses a launchd daemon (the most secure way), the code is available on google code. So if you want, you can copy that code.

I think I can now answer this, thanks to some Googling and a nice find in this SO question. It's very slightly hacky, but IMHO is a satisfactory solution.
I wrote this generic implementation which should achieve what you want:
- (BOOL) runProcessAsAdministrator:(NSString*)scriptPath
withArguments:(NSArray *)arguments
output:(NSString **)output
errorDescription:(NSString **)errorDescription {
NSString * allArgs = [arguments componentsJoinedByString:#" "];
NSString * fullScript = [NSString stringWithFormat:#"%# %#", scriptPath, allArgs];
NSDictionary *errorInfo = [NSDictionary new];
NSString *script = [NSString stringWithFormat:#"do shell script \"%#\" with administrator privileges", fullScript];
NSAppleScript *appleScript = [[NSAppleScript new] initWithSource:script];
NSAppleEventDescriptor * eventResult = [appleScript executeAndReturnError:&errorInfo];
// Check errorInfo
if (! eventResult)
{
// Describe common errors
*errorDescription = nil;
if ([errorInfo valueForKey:NSAppleScriptErrorNumber])
{
NSNumber * errorNumber = (NSNumber *)[errorInfo valueForKey:NSAppleScriptErrorNumber];
if ([errorNumber intValue] == -128)
*errorDescription = #"The administrator password is required to do this.";
}
// Set error message from provided message
if (*errorDescription == nil)
{
if ([errorInfo valueForKey:NSAppleScriptErrorMessage])
*errorDescription = (NSString *)[errorInfo valueForKey:NSAppleScriptErrorMessage];
}
return NO;
}
else
{
// Set output to the AppleScript's output
*output = [eventResult stringValue];
return YES;
}
}
Usage example:
NSString * output = nil;
NSString * processErrorDescription = nil;
BOOL success = [self runProcessAsAdministrator:#"/usr/bin/id"
withArguments:[NSArray arrayWithObjects:#"-un", nil]
output:&output
errorDescription:&processErrorDescription
asAdministrator:YES];
if (!success) // Process failed to run
{
// ...look at errorDescription
}
else
{
// ...process output
}

Okay, so I ran into this while searching how to do this properly... I know it's probably the least secure method of accomplishing the task, but probably the easiest and I haven't seen this answer anywhere. I came up with this for apps that I create to run for my own purposes and as a temporary authorization routine for the type of task that user142019 is describing. I don't think Apple would approve. This is just a snippet and does not include a UI input form or any way to capture stdout, but there are plenty of other resources that can provide those pieces.
Create a blank file called "script.sh" and add it to your project's supporting files.
Add this to header file:
// set this from IBOutlets or encrypted file
#property (strong, nonatomic) NSString * password;
#property (strong, nonatomic) NSString * command;
implementation:
#synthesize password;
#synthesize command;
(IBAction)buttonExecute:(id)sender {
NSString *scriptPath = [[NSBundle mainBundle]pathForResource:#"script" ofType:#"sh"];
NSString *scriptText = [[NSString alloc]initWithFormat:#"#! usr/sh/echo\n%# | sudo -S %#",password,command];
[scriptText writeToFile:scriptPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
NSTask * task = [[NSTask alloc]init];
[task setLaunchPath:#"/bin/sh"];
NSArray * args = [NSArray arrayWithObjects:scriptPath, nil];
[task setArguments:args];
[task launch];
NSString * blank = #" ";
[blank writeToFile:scriptPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
The last step is just so you don't have a cleartext admin password sitting in your bundle. I recommend making sure that anything beyond the method be obfuscated in some way.

In one of my cases this is not correct:
>
The problem is that simply using Substitute User Do (sudo) doesn't work as the user needs to enter the password
>
I simply edited /etc/sudoers to allow the desired user to start any .sh script without prompting for password. So you would execute a shell script which contains a command line like sudo sed [...] /etc/printers.conf to modify the printers.conf file, and the /etc/sudoers file would contain this line
myLocalUser ALL=(ALL) NOPASSWD: ALL
But of course I am looking for a better solution which correctly prompts the user to type in an admin password to allow the script or NSTask to execute. Thanks for the code which uses an AppleScript call to prompt and execute the task/shell script.

Related

Using AppleScript with administrator privileges, "not ask for authentication again for five minutes." doesn't work?

in Apple Document Technical Note 2065, it mention "do shell script command with administrator privileges",when using this way,"Once a script is correctly authenticated, it will not ask for authentication again for five minutes."
but, it still need ask for authentication again and again.
I find,when use ScriptEditor.app, the Apple Document is right.
eg:
do shell script "/bin/cp -r /Users/Simon/Desktop/Test/test.zip /Users/Simon/Desktop/ " with administrator privileges
but, when use NSAppleScript running shell script, the Apple Document is wrong.
eg:
NSDictionary *error = nil;
NSString *copyScript = #"do shell script \"/bin/cp -r /Users/Simon/Desktop/Test/test.zip /Users/Simon/Desktop \" with administrator privileges";
NSAppleScript *copyAppleScript = [[NSAppleScript alloc] initWithSource:copyScript];
if ([copyAppleScript executeAndReturnError:&error])
{
NSLog(#"copyAppleScript Success!");
}
else
{
NSLog(#"copyAppleScript Failuer!");
}
I hope, when do shell script with NSAppleScript, it will not ask for authentication again for five minutes also.
First, I don't think this is the best way to approach this problem. But let me answer the question first, and then suggest another avenue.
Note that tech note 2065 says this:
The authentication only applies to that specific script: a different
script, or a modified version of the same one, will ask for its own
authentication.
Every time you run this line:
NSAppleScript *copyAppleScript = [[NSAppleScript alloc] initWithSource:copyScript];
...you create a new script, and that new script will need authorization of its own. If you want to reuse a given script without constantly reauthorizing, you'll need to create an NSAppleScript property, store your script there once, and then call it repeatedly. In other words:
#interface MyObject ()
#property (strong) NSAppleScript *myAppleScript;
#end
#implementation MyObject
- (void)setUpScript {
// call this once
NSDictionary *error = nil;
NSString *copyScript = #"do shell script \"/bin/cp -r /Users/Simon/Desktop/Test/test.zip /Users/Simon/Desktop \" with administrator privileges";
self.myAppleScript = [[NSAppleScript alloc] initWithSource:copyScript];
}
- (void)runScript {
// call this as needed; shouldn't need reauthorization
if ([self.myAppleScript executeAndReturnError:&error]) {
NSLog(#"myAppleScript Success!");
} else {
NSLog(#"myAppleScript Failure!");
}
}
#end
However, since your goal here is to run a shell script, I'd suggest you forget about NSAppleScript entirely, and use NSTask instead. NSTask is how shell scripts are meant to be run from objC, and should avoid authorization problems entirely. The easiest way to do this is to write an explicit shell script and store it in the bundle:
#!/bin/bash
cp -r /Users/Simon/Desktop/Test/test.zip /Users/Simon/Desktop
Then call and run that shell script:
NSError *err;
NSTask *task = [NSTask launchedTaskWithExecutableURL:[[NSBundle mainBundle] URLForResource:#"ShellScript"
withExtension:#"sh"]
arguments:[NSArray array]
error:&err
terminationHandler:nil];
...or maybe this, if you don't want to bundle a script...
NSError *err;
NSTask *task = [[NSTask alloc] init]
task.arguments = [NSArray arrayWithObjects:#"#!/bin/bash",
#"-c",
#"cp -r /Users/Simon/Desktop/Test/test.zip /Users/Simon/Desktop",
nil];
[task launchAndReturnError:&err];
P.S.
I've been assuming that this cp command is just proof-of-concept, but if your actual goal is to copy a file, forget about NSAppleScript and NSTask. Use NSFileManager's copyItemAtURL:toURL:error: instead.

Failed attempt using Related Items to create backup file in sandboxed app

The App Sandbox design guide says:
The related items feature of App Sandbox lets your app access files
that have the same name as a user-chosen file, but a different
extension. This feature consists of two parts: a list of related
extensions in the application’s Info.plist file and code to tell the
sandbox what you’re doing.
My Info.plist defines a document type for .pnd files (the user-chosen file), as well as a document type for .bak files. The entry for the .bak files has, among other properties, the property NSIsRelatedItemType = YES.
I am trying to use Related Items to move an existing file to a backup file (change .pnd suffix to .bak suffix) when the user writes a new version of the .pnd file. The application is sandboxed. I am not proficient with sandboxing.
I am using PasteurOrgManager as the NSFilePresenter class for both the original and backup files:
#interface PasteurOrgData : NSObject <NSFilePresenter>
. . . .
#property (readonly, copy) NSURL *primaryPresentedItemURL;
#property (readonly, copy) NSURL *presentedItemURL;
#property (readwrite) NSOperationQueue *presentedItemOperationQueue;
#property (readwrite) NSFileCoordinator *fileCoordinator;
. . . .
- (void) doBackupOf: (NSString*) path;
. . . .
#end
The doBackupOf: method is as follows. Notice that it also sets the NSFilePresenter properties:
- (void) doBackupOf: (NSString*) path
{
NSError *error = nil;
NSString *appSuffix = #".pnd";
NSURL *const pathAsURL = [NSURL URLWithString: [NSString stringWithFormat: #"file://%#", path]];
NSString *const baseName = [pathAsURL lastPathComponent];
NSString *const prefixToBasename = [path substringToIndex: [path length] - [baseName length] - 1];
NSString *const baseNameWithoutExtension = [baseName substringToIndex: [baseName length] - [appSuffix length]];
NSString *backupPath = [NSString stringWithFormat: #"%#/%#.bak", prefixToBasename, baseNameWithoutExtension];
NSURL *const backupURL = [NSURL URLWithString: [NSString stringWithFormat: #"file://%#", backupPath]];
// Move backup to trash — I am sure this will be my next challenge
// (it's a no-op now because there is no pre-existing .bak file)
[[NSFileManager defaultManager] trashItemAtURL: backupURL
resultingItemURL: nil
error: &error];
// Move file to backup
primaryPresentedItemURL = pathAsURL;
presentedItemURL = backupURL;
presentedItemOperationQueue = [NSOperationQueue mainQueue];
[NSFileCoordinator addFilePresenter: self];
fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter: self]; // error here
[self backupItemWithCoordinationFrom: pathAsURL
to: backupURL];
[NSFileCoordinator removeFilePresenter: self];
fileCoordinator = nil;
}
The backupItemWithCoordinationFrom: method does the heavy lifting, basically:
[fileCoordinator coordinateWritingItemAtURL: from
options: NSFileCoordinatorWritingForMoving
error: &error
byAccessor: ^(NSURL *oldURL) {
[self.fileCoordinator itemAtURL: oldURL willMoveToURL: to];
[[NSFileManager defaultManager] moveItemAtURL: oldURL
toURL: to
error: &error];
[self.fileCoordinator itemAtURL: oldURL didMoveToURL: to];
}
but the code doesn't make it that far. I have traced the code and the URL variables are as I expect, and are reasonable. At the point of "error here" in the above code, where I allocate the File Presenter, I get:
NSFileSandboxingRequestRelatedItemExtension: an error was received from pboxd instead of a token. Domain: NSPOSIXErrorDomain, code: 1
[presenter] +[NSFileCoordinator addFilePresenter:] could not get a sandbox extension. primaryPresentedItemURL: file:///Users/cope/Me.pnd, presentedItemURL: file:///Users/cope/Me.bak
Any help is appreciated.
(I have read related posts Where can a sandboxed Mac app save files? and Why do NSFilePresenter protocol methods never get called?. I have taken note of several other sandboxing-related posts that don't seem relevant to this issue.)
MacBook Pro, MacOS 10.13.5, XCode Version 9.3 (9E145)
do not read too much about avoiding sandboxing. Most explenations go too far out of the most obvious problem. Instead of explaining the pitfalls that rightfully triggers sandboxing they explain mostly how to avoid the Sandbox at all. Which is not a solution - it is a thread!
So the most obvious problem is exposing a URL to pasteboard that still needs properly escaped characters in the string before you transform to NSURL.
So your NSString beginning with "file://" should use something like..
NSString *encodeStringForURL = [yourstring stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
before you transform to NSURL with
NSURL *fileurl = [NSURL URLWithString:encodeStringForURL];
NString *output = fileurl.absoluteString;

Xcode 8 extension executing NSTask

My goal is to create an extension that executes clang-format. My code looks something like this:
- (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation completionHandler:(void (^)(NSError * _Nullable nilOrError))completionHandler
{
NSError *error = nil;
NSURL *executableURL = [[self class] executableURL];
if (!executableURL)
{
NSString *errorDescription = [NSString stringWithFormat:#"Failed to find clang-format. Ensure it is installed at any of these locations\n%#", [[self class] clangFormatUrls]];
completionHandler([NSError errorWithDomain:SourceEditorCommandErrorDomain
code:1
userInfo:#{NSLocalizedDescriptionKey: errorDescription}]);
return;
}
NSMutableArray *args = [NSMutableArray array];
[args addObject:#"-style=LLVM"];
[args addObject:#"someFile.m"];
NSPipe *outputPipe = [NSPipe pipe];
NSPipe *errorPipe = [NSPipe pipe];
NSTask *task = [[NSTask alloc] init];
task.launchPath = executableURL.path;
task.arguments = args;
task.standardOutput = outputPipe;
task.standardError = errorPipe;
#try
{
[task launch];
}
#catch (NSException *exception)
{
completionHandler([NSError errorWithDomain:SourceEditorCommandErrorDomain
code:2
userInfo:#{NSLocalizedDescriptionKey: [NSString stringWithFormat:#"Failed to run clang-format: %#", exception.reason]}]);
return;
}
[task waitUntilExit];
NSString *output = [[NSString alloc] initWithData:[[outputPipe fileHandleForReading] readDataToEndOfFile]
encoding:NSUTF8StringEncoding];
NSString *errorOutput = [[NSString alloc] initWithData:[[errorPipe fileHandleForReading] readDataToEndOfFile]
encoding:NSUTF8StringEncoding];
[[outputPipe fileHandleForReading] closeFile];
[[errorPipe fileHandleForReading] closeFile];
int status = [task terminationStatus];
if (status == 0)
{
NSLog(#"Success: %#", output);
}
else
{
error = [NSError errorWithDomain:SourceEditorCommandErrorDomain
code:3
userInfo:#{NSLocalizedDescriptionKey: errorOutput}];
}
completionHandler(error);
}
The reason I need that try-catch block is because an exception is thrown when I try to run this code. The exception reason is:
Error: launch path not accessible
The path for my clang-format is /usr/local/bin/clang-format. What I discovered is that it doesn't like me trying to access an application in /usr/local/bin, but /bin is ok (e.g. If I try to execute /bin/ls there is no problem).
Another solution I tried was to run /bin/bash by setting the launch path and arguments like this:
task.launchPath = [[[NSProcessInfo processInfo] environment] objectForKey:#"SHELL"];
task.arguments = #[#"-l", #"-c", #"/usr/local/bin/clang-format -style=LLVM someFile.m"];
This successfully launches the task, but it fails with the following error output:
/bin/bash: /etc/profile: Operation not permitted
/bin/bash: /usr/local/bin/clang-format: Operation not permitted
The first error message is due to trying to call the -l parameter in bash, which tries to log in as the user.
Any idea how I can enable access to those other folders? Is there some kind of sandbox environment setting I need to enable?
I guess that because of the sandboxing this is not possible.
You could bundle the clang-format executable and use it from there.
Personally, I think you are going at it all wrong. Extensions are supposed to be quick (if you watch the video on Xcode extensions he repeats multiple times to get in and get out). And they are severely limited.
However, there is another - the container app may be able to do this processing for your extension without all the hacks. The downside is that you have to pass the buffer to and from the extension.
It’s not easy, but it can be done. Easy peasy way to get your container to run. First, modify the container app’s Info.plist (not the extension Info.plist) so that it has a URL type.
In your extension you can “wake up” the container app by running the following:
let customurl = NSURL.init(string: “yoururlschemehere://")
NSWorkspace.shared().open(customurl as! URL)
As for communication between the two, Apple has a plethora of methods. Me, I’m old-school, so I’m using DistributedNotificationCenter - for the moment.
Although I haven’t tried it, I do not see why the container app should have an issue chatting with clang (I’m using the container app for settings).

NSTask: program launching another program, how to do?

I want to do with an NSTask what I am able to do in the terminal via
$ myprogram myfile.ext
I know that myprogram (I don't have any control on this program) launches another program myauxprogram. Furthermore, the path to myprogram is path1 and the path to myprogram is path2.
If I do
NSTask * myTask = [[NSTask alloc] init];
NSArray * arguments = #[#"myfile.ext"] ;
[myTask setCurrentDirectoryPath:[URLOfTheFolder path]];
[myTask setLaunchPath:#"/path1/myprogram"];
[myTask setArguments:arguments];
[myTask launch] ;
I get the following error sh: myauxprogam: command not found
If I create a symbol link in path1 to myauxprogram, the problem is the same.
It looks like you mostly have the idea, you're just missing a few key elements. NSTask should have a defined LaunchPath, such as /usr/bin/sh, /usr/bin, etc. from there depending on the type of path you're requiring you set up your arguments and options.
NSTask *auxTask;
NSTask *auxPath = #"./path1/myprogram"; // period might be needed
NSTask *auxArgs = #"myfile.ext";
auxTask = [[NSTask alloc] init];
[auxTask setLaunchPath:#"/usr/bin/sh"]; // use shell
[auxTask setArguments:[NSArray arrayWithObjects:
#"-c", // -c will exec as shell cmd
auxPath,
auxArgs,
nil]];
// using `try` is optional
#try
{
[auxTask launch];
}
#catch(id exc)
{
auxTaskSuccess = NO;
}
[auxTask release];
return;
No idea if this works, but this is the way you would likely set things up. If you run into issues with errors or the NSTask failing you can set up an NSLog with the auxTask NSArray to see exactly what it's doing.
The problem was:
You need to [myTask setEnvironment:...] so that the task knows which are the environment variables (PATH, etc.)

Saving System Profiler info in a .spx file using NSTask

In Cocoa, I am trying to implement a button, which when the user clicks on will capture the System profiler report and paste it on the Desktop.
Code
NSTask *taskDebug;
NSPipe *pipeDebug;
taskDebug = [[NSTask alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:selfselector:#selector(taskFinished:) name:NSTaskDidTerminateNotification object:taskDebug];
[profilerButton setTitle:#"Please Wait"];
[profilerButton setEnabled:NO];
[taskDebug setLaunchPath: #"/usr/sbin/system_profiler"];
NSArray *args = [NSArray arrayWithObjects:#"-xml",#"-detailLevel",#"full",#">", #"
~/Desktop/Profiler.spx",nil];
[taskDebug setArguments:args];
[taskDebug launch];
But this does not save the file to the Desktop. Having
NSArray *args = [NSArray arrayWithObjects:#"-xml",#"-detailLevel",#"full",nil]
works and it drops the whole sys profiler output in the Console Window.
Any tips on why this does not work or how to better implement this ? I am trying to refrain from using a shell script or APpleScript to get the system profiler. If nothing work's that would be my final option.
Thanks in advance.
NSArray *args = [NSArray arrayWithObjects:#"-xml",#"-detailLevel",#"full",#">", #"~/Desktop/Profiler.spx",nil];
That won't work because you aren't going through the shell, and > is a shell operator. (Also, ~ isn't special except when you expand it using stringByExpandingTildeInPath.)
Create an NSFileHandle for writing to that Profiler.spx file, making sure to use the full absolute path, not the tilde-abbreviated path. Then, set that NSFileHandle as the task's standard output. This is essentially what the shell does when you use a > operator in it.
This got it done ( thanks to Peter and Costique)
[taskDebug setLaunchPath: #"/usr/sbin/system_profiler"];
NSArray *args = [NSArray arrayWithObjects:#"-xml",#"- detailLevel",#"full",nil];
[taskDebug setArguments:args];
[[NSFileManager defaultManager] createFileAtPath: [pathToFile stringByExpandingTildeInPath] contents: nil attributes: nil];
outFile = [ NSFileHandle fileHandleForWritingAtPath:[pathToFile stringByExpandingTildeInPath]];
[taskDebug setStandardOutput:outFile];
[taskDebug launch];
Create an NSPipe, send [taskDebug setStandardOutput: myPipe] and read from the pipe's file handle.

Resources