Dialog is a program you can call with arguments specifying what you want to display (input box, menu…).
Here is an example of the result of the « dialog --msgbox "Hello world.\n\nHow are you ?" 0 0 » command :
During a system audit, we had access to a server that only had a command-line GUI interface. This interface presented menus allowing us to do various diagnostics to the server (that would end up executing scripts)...
Of course, our goal was to find a bug in this interface to execute arbitrary code on the server.
After a little analysis, we found out that the whole script for the GUI interface was calling the « dialog » command to display the various dialog presented to the user.
From parameter injection...We encountered code to execute a diagnostic script, providing it a number as a parameter :
With the dialog_input function being like :
n = "" while not n.isdigit(): n = dialog_input("Enter a number", n) system("/root/diagnostic_script %s" % n)
So, the more obvious way would have been to put a string like "; whoami" in the dialog, to have the system() execute our code.
def dialog_input(question, default): args = shlex.split('dialog --inputbox "%s" 10 50 "%s"' % (question, default)) _, stderr = subprocess.Popen(args, stderr=subprocess.PIPE).communicate() return stderr.decode()
However, this is made impossible because the number is validated, and if we enter an invalid number we will be asked again.
But what is really interesting is that the old number we entered (that is invalid !) will be set as the default value of the input, by passing it as an argument for the dialog command.
So, what happens if we put a quote in the number we enter ? As it is invalid, dialog_input() is called again, and booom, shlex.split is lost :
So, it's obvious that from now, we can inject arguments for the « subprocess.Popen » call (as long as we put an even number of quotes in the dialog).
Traceback (most recent call last): ... ValueError: No closing quotation
The scope of what we can do is very limited. We can't use shell tricks like a semicolon or a pipe to execute other commands : subprocess.Popen directly call the « dialog » command (as the shell argument is False by default), so we can only control its parameters.
So, we have to dig into how dialog works. We can see that calls like « dialog --inputbox "foo" 10 50 "bar" --inputbox "foo2" 10 50 "bar2" » works fine, as dialog iteratively parse its argument and allow multiple widgets to be displayed successively.
Let's see if we can find some useful widgets ! After reading the manual, there is « --dselect » that is a directory chooser (so we can see the content of the filesystem), and « --textbox » that can display the content of a file :)
So, we have arbitrary (read) access to the filesystem !
... to arbitrary command execution...However, we also found one very interesting widget : prgbox.
So, this widget was designed… to execute any command.
--prgbox command height width A prgbox is very similar to a programbox. This dialog box is used to display the output of a command that is specified as an argument to prgbox. After the command completes, the user can press the ENTER key so that dialog will exit and the calling shell script can continue its operation.
However, during our trials it was impossible to have it work correctly because each time we put shell commands the behavior was completely unexpected, often resulting in an empty result.
...by working around a nasty bug !We looked at the source code of the prgbox widget and found that :
Which shown clearly that our command was executed using « sh -c command », so using shell should have worked !
sprintf(blob, "-c %s", command); argv = dlg_string_to_argv(blob); // will convert the command in « blob » to a list of arguments. execvp("sh", argv);
However, shouldn't argv contain « sh » ? It is this !
When « sh » was called, its argv, instead of containing « sh », was « -c » (so it ignored this argument, thinking it was its name !).
A quick workaround to allow our exploit to work was to insert « -c » at the beginning of the command we passed to the prgbox widget, for sh to correctly have « -c » as its argv.
So, at the end we were able to spawn a backdoor shell in our target server, simply by entering this in the input box asking for the IP address to ping :
" --prgbox "-c \"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc -l 8000 >/tmp/f\"" 30 70 --title "
We also contacted the author of dialog to inform him of the bug, that was corrected in the last version of dialog.
It was also very funny to dig into the dialog source code, and see that there is even an option « --file » that allows an user to put more arguments in a file (they will be inserted in place during the process of parsing the arguments). And putting another « --file » option referencing to itself inside such a file could have led to infinite recursion ! So the program contains a counter of the recursion depth and don't allow it to be more than 20.
ConclusionWhen you use an external program like dialog, be very careful with what you allow the user to inject in the parameters. As we have just seen, even if the command is not interpreted in the shell, it is possible to misuse the program you are calling to end up executing arbitrary code.
And when we search github for misuse examples, it seems that it is quite easy to encounter…