Preface
I keep feeling that using a domain name just to host a blog is a bit wasteful—after all, it costs 60RMB/Year, and the VPS’s idle bandwidth and compute resources should be put to good use too. Since multiple third-level subdomains can be bound, it’s entirely feasible to build a more complex personal site and provide different services under different domains. The VPS is already running Aria2 and Transmission; I’m planning to write an information-collection crawler for fun, and later things like AI adversarial stuff could also run on the VPS. With that kind of planning, a fairly distinctive small site starts to take shape. A journey of a thousand miles begins with a single step—let’s get the homepage done first!
So I went to W3layouts to look for homepage templates. There were so many that it was dazzling, but unfortunately I lack the “artistic bacteria” to modify them. In the end I finally found one with a minimalist style, but it looked a bit rigid and plain, with little dynamic content. After thinking about it, I decided I still had to rework it myself—thus began this round of tinkering.
Bringing Up the Problem
First, the template had a Recent Posts section, and my perfectionism kicked in: I wanted its content to update automatically. The problem is that I use Pelican, which is clearly a static blog generator! With no alternative, I modified it myself; details are in the appendix. In short, the first battle was a win.
Next, I noticed the template’s Contact Me section, and once again I was struck: this is great! Since I have a VPS, naturally I can make it more powerful. How about having visitors’ messages automatically emailed to me after they submit them? Not a bad idea.
It seemed simple: on the server side, write some PHP to receive POST parameters, then call mutt directly to send the email. I quickly looked up basic PHP syntax—indeed the best language in the universe—and hacked together a prototype in a few lines. Excitedly I clicked run, and… damn?! Send failed?! How could it possibly fail?! No matter how carefully I checked, I couldn’t figure out what I’d done wrong. And there weren’t even logs to look at, because this build of mutt had debug disabled at compile time. While I was stuck, it suddenly occurred to me: when Apache executes external programs, it must be using the permissions of the apache user… but does that user have permission to run mutt?
So I tried a simpler command: query mutt’s version and return it to the page. It worked fine, which meant the apache user definitely could invoke mutt. But why did sending mail fail? At that point I finally thought of using su -c: run the command as apache and see why it fails. It turned out that when mutt runs, by default it writes log information to the sent file under the user’s HOME directory, but apache is a nologin user. If it can’t log in, it doesn’t really have a home directory, so it can’t write that log file, and sending fails.
So the remaining question became straightforward: how do you execute a program under Apache with the permissions of some other normal user?
Privilege Escalation
Complaint mode off; now to the serious part. On *nix systems, there is a clever and legitimate way to elevate privileges: SetUID. Roughly speaking, for an executable with the SetUID bit set, the executor automatically gains the privileges of the file’s owner. In other words, if rm were set with this bit (and the file is owned by root), then any process invoking it would run with root privileges—meaning it could delete system files at will!
Back to our case: the SetUID feature is not for destruction. If we write a program specifically to invoke another program based on command-line arguments, set its owner to a normal user (say test), and then set the SetUID bit, then when we call this program from PHP as apache to execute a new command, that command should run with test’s privileges by default, right?
In fact, when we implement such a program (owned by test with SetUID enabled) and call it as apache to run whoami, we’ll sadly find it still returns "apache". That is, the apache user is not able to use a SetUID program owned by test to masquerade as test when executing commands. Why?
To explain this, we introduce the concepts of real UID and effective UID. As the names imply, the former is the real UID, and the latter is the current effective UID. In practice, when we execute a program with the SetUID bit set, what changes is only the process’s effective UID, so that the current process gains another user’s privileges; but the process’s real UID can still reveal its origin—this process is merely a user with real UID who invoked a SetUID program, and thus currently has this effective UID. This design ensures that, on the one hand, the program can gain the new user’s privileges during execution (via effective UID), while on the other hand it does not lose the privileges it originally had (via real UID).
Below is a StackOverflow quote explaining three kinds of UIDs in detail:
Real UID
This is the UID of the user/process that created THIS process. It can be changed only if the running process has EUID=0.
Effective UID
This UID is used to evaluate privileges of the process to perform a particular action. EUID can be changed either to RUID, or SUID if EUID!=0. If EUID=0, it can be changed to anything.
Saved UID
If you run an executable with the set-UID bit set, then the resulting running process will start off with a real UID of the real user running it, and an effective and saved UID of the owner of the executable file. If the process then calls setuid() or seteuid() to change their effective UID, they can still get back their original privileges again thanks to the saved UID. If the set-UID bit is not set, SUID will be the RUID.
Next, let’s analyze why the behavior in the example differs from what we expected. First, we know that common calls like system() (or similar) in various languages are essentially just wrappers around the system-level fork()->exec() process. fork() duplicates a child process, and then exec() is called within the child to replace the current process image with a new program. Second, we also know that when exec() replaces the current process, if the program does not have SetUID set, then the new process’s UID/EUID are both equal to the original process’s UID.
Therefore, when the apache process calls the SetUID program above, the current process’s EUID becomes test, while the UID remains apache. Then, when system() is invoked within the current process, it first duplicates a child process, and the parent and child’s UID and EUID remain unchanged; next the child calls exec() to replace itself. Since the executed program does not have SetUID set, the new process’s UID and EUID both become the UID of the original caller process, namely the apache user. Because whoami displays the EUID, the output is "apache" rather than "test".
Now we can see how to truly obtain a new process whose UID and EUID are both test: just swap the UID and EUID in time, so that when system() is called, the original process’s UID is already test; then the new process’s UID and EUID will both become test. Sample code is in the appendix.
system() Calls and login shell
For shell programs like bash, a login shell means a shell environment that loads configuration files, environment variables, and so on. When we use su - username to switch users, the hyphen indicates enabling a login shell after switching users. Many programs require a series of environment variables to run correctly, meaning they must run in a login shell. I had already hit this pitfall earlier when using Python’s subprocess library.
Therefore, whether in C, or in scripting languages like Python and PHP, when making system()-like calls, for safety it’s best to ensure the command executes inside a login shell. My previous idea was to use su - username -c specific_command to switch to a normal user and run it, but that requires running system() with root privileges; otherwise it will fail because a password is needed. A simpler approach I came up with is: bash --login -c specific_command to spawn a login shell. This does not require special privileges and does not introduce security risks. The specific implementation is also in the appendix C code.
Analyzing the Command-Line Argument Passing Process
Here we briefly analyze how the comment data posted from the frontend is passed down layer by layer and ultimately calls mutt. First, when the PHP code runs, it reads the POST data from the HTTP environment variables, then immediately saves the data to a temporary file. Next, it constructs a command to read the data from the temporary file and send it to the specified mailbox. But note that this command is just a string and has not been executed.
We pass this command string as a command-line argument to /usr/local/bin/RunAsUser, which is the program used for privilege switching, executing the passed parameters as a normal user. However, inside this program the command string is further wrapped to ensure the command runs within a login shell, as described above. Finally, the packaged command is handed to system(), and only then does it actually start invoking mutt to send the email.
Appendix
PHP mail-sending sample code
<?php
$text = $_POST['text'];
$name = $_POST['name'];
if (!empty($text)) {
$temp = tempnam("/tmp","PHP_TEMP_");
$fid = fopen($temp, "w");
fwrite($fid, $text);
fclose($fid);
$cmd = "/usr/local/bin/RunAsUser /usr/bin/mutt -s \'New Blog Comment by ".$name."\' -e \' set content_type=\'text/html\' \' -e \' set realname=\'".$name."\' \' 852301601@qq.com < ".$temp;
system($cmd, $status);
unlink($temp);
if ( $status == 0 ) echo "Your message has been sent successfully";
else echo "System error: sending failed. Please send the email manually.";
} else {
echo "Please use POST method.";
}
?>
Privilege-changing sample code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#define MAX 2048
char *fmt = "bash --login -c \"%s\"";
char cmd[MAX + 128] = "";
char buf[MAX];
int main( int argc, char *argv[] )
{
int i, sum = 0, res = 0;
uid_t uid , euid;
uid = getuid();
euid= geteuid();
//The following step is the core!
if(setreuid(euid,uid)) perror("setreuid");
for ( i = 1; i < argc; i++ ) sum += strlen(argv[i]);
if ( sum + argc > MAX ) {
puts("Command too long!");
return -1;
}
buf[0] = 0;
for ( i = 1; i < argc; i++ ) {
if ( i != 1 ) strcat(buf, " ");
strcat(buf, argv[i]);
}
sprintf(cmd, fmt, buf);
res = system(cmd);
//puts(cmd);
return res;
}
Some usage details of mutt
If you need to override certain settings at runtime, you can directly use the form -e 'set xxx=\'xxx\''. Pay attention to character escaping; alternatively, you can also alternate single and double quotes. If multiple statements need to be executed, use multiple -e options, rather than writing the commands together.
By default, mutt cannot correctly handle Chinese encodings. You need to add the following lines to the config file /etc/Muttrc.local:
set charset="utf-8"
set send_charset="gb2312"
set send_charset="utf-8"
Typically you can send mail directly with something like echo "Email body" | mutt -s Subject xxx@xxx, but it is easy to run into errors. If the body contains CJK characters, line breaks, etc., sending may fail. Therefore, when sending mail via PHP, you should not construct a one-line send statement directly; instead, save the email body to a temporary file first, then read the mail content from that file and send it.
Showing recent posts in a Pelican blog
Since the blog is an important part of a personal site, showing recently updated posts on the homepage is a good choice. For “heavyweight” blogs like Wordpress, because the backend uses a database, it is very easy to customize the recent posts shown on the homepage—just do a database select. But for a static-generated blog like Pelican, displaying recent posts on the homepage is a bit more troublesome.
Considering Pelican’s extensibility, there are two main parts that are convenient to modify: templates referenced when generating HTML, and plugins invoked during preprocessing. After comparison and analysis, I chose to use the former. In short, I defined a template whose generated HTML content is actually JSON data. Then when the homepage loads, it runs JavaScript and uses jQuery’s GET method to load this structured JSON data into HTML tags, so the recently updated posts can be displayed perfectly.
At this point another problem arises: since the blog address generally uses a third-level subdomain while the site homepage is a second-level domain, as shown below, the homepage directory stores the homepage data and is bound to finaltheory.me; the blog directory stores the blog data and is bound to blog.finaltheory.me.
/var/www/html/
├── ai
├── aria2
├── blog
├── downloads
├── homepage
└── wordpress
When Pelican updates, it can only update the generated HTML into the blog directory bound to the third-level subdomain. If the homepage wants to access it, it becomes a cross-origin request. Given that the server is for my personal use, and I generally won’t change the directory structure casually, I didn’t bother with other approaches; I simply made a symlink with ln -s ../blog/abstracts.html and called it done.
Domain-to-directory binding in Apache and its priority
Users of virtual hosts often overlook a puzzling question: everyone uses the same host to provide services, and all domains point to the same IP address—so why does visiting different domains reach different websites? In fact, server programs like Apache automatically decide, based on the URL information in the request, which directory’s scripts to execute or which directory’s data to return. In other words, on the server side, different domains can be bound to different directories, enabling one machine to host multiple sites.
For CentOS 7’s httpd, user custom configurations are generally stored under /etc/httpd/conf.d, and each .conf file there is included into the main configuration. We use the VirtualHost directive to define the mapping between directories and domains. Its basic syntax is as follows:
<VirtualHost *:80>
ServerAdmin FinalTheory@hotmail.com
DocumentRoot /var/www/html/blog
ServerName blog.finaltheory.me
ErrorDocument 404 /404.html
</VirtualHost>
Here, DocumentRoot is the directory path, and ServerName is the domain to bind. This binding supports both second-level and third-level domains. If a second-level domain is bound to a directory, then when accessing any unbound third-level subdomains under that second-level domain (assuming the DNS records already point to the server), Apache will by default route them to the directory bound to the second-level domain.
For example, this blog’s second-level domain is finaltheory.me, bound to the homepage directory shown above. If a new third-level domain test.finaltheory.me is added and DNS is configured, then visiting that domain will by default return the content under the homepage directory. Also, you must restart Apache for the directory binding to take effect!
Because browsers like Chrome have caching mechanisms and will cache some static content, updates may not appear promptly. Therefore, when checking whether the directory binding has taken effect or is correct, judging by refreshing the page in the browser is unreliable. A simple approach is to use the no-cache urlopen function directly in IPython to check whether the returned data matches expectations:
print urlopen('http://www.baidu.com').read()。