为了了解 PHP、JSP、ASP 出现之前人们写网站的方法,洒家研究了一波 CGI,使用 C、Python、batch、shell script 语言写了几个简单的网页。
CGI 即通用网关接口,指 Web 服务器调用编程语言编写的程序的一个接口。洒家用的是 Apache 的 CGI,QUERY_STRING
、REMOTE_ADDR
、REQUEST_URI
等参数是通过环境变量传递给 CGI 程序的,请求主体(POST 数据)作为 CGI 程序的标准输入(stdin),而 CGI 程序的标准输出(stdout)作为 HTTP 响应的部分(注:标准错误输出 stderr 会出现在错误日志中)。
系统和软件环境配置¶
WAMP Apache 2.4.18 64 位,Ubuntu Server 16.04 64 位。
需要开启 Apache 的 cgi_module
。
# a2enmod cgi
# service apache2 restart
一般在浏览器中通过访问 /cgi-bin/
来访问开启了 CGI 执行权限的目录。对于 Ubuntu,/etc/apache2/conf-enabled/serve-cgi-bin.conf
配置文件中规定了 URL /cgi-bin/
对应的实际路径,默认为 /usr/lib/cgi-bin/
。Windows 下的 WAMP 套件的 CGI 目录则默认位于安装目录内(例如 C:/wamp64/cgi-bin/
)。
坑¶
由于洒家对 Linux 不熟悉,踩到了几个坑
- 对于 Ubuntu 中 shell script,开头的
#!/bin/sh
和#!/bin/bash
是不同的。Ubuntu 的/bin/sh
是/bin/dash
(Debian Almquist shell
)的链接。对于echo -e 'Content-Type: abc\n'
的执行结果不同。 - HTTP 响应头和响应主体之间要有一个换行符,否则无法分清响应主体和响应头部。
- Linux 上的程序要加可执行文件权限。否则报 500 错误
Permission denied: exec of '/usr/lib/cgi-bin/env_var.sh' failed: /usr/lib/cgi-bin/env_var.sh
。 - shell script 执行字符串要用
eval
,否则不能把引号中带空格的字符串识别为同一个参数。
参考¶
用 C、Python、Shell Script、batch(批处理)写的几个小程序¶
编程语言 Perl 是一个广泛被用来编写 CGI 程序的语言,但 CGI 的一个目的是要独立于任何语言的。Web 服务器无须在这个问题上对语言有任何了解。事实上,CGI 程序可以用任何脚本语言或者是完全独立编程语言实现,只要这个语言可以在这个系统上运行。除 Perl 外,像 Unix shell script, Python, Ruby, PHP, Tcl, C/C++,和 Visual Basic 都可以用来编写 CGI 程序。
洒家看到这段话的时候想,哇塞,还可以用 C 语言写网站!洒家顿时感到了历史的气息。
查看所有的环境变量¶
在 Linux 下可以用 env 命令,列出所有的环境变量。注意,HTTP 响应头和响应主体之间要有一个换行符,否则无法分清响应主体和响应头部,报错:malformed header from script 'env_var.sh': Bad header: SERVER_SIGNATURE=<address>Apac
。
使用 shell script 脚本:
#!/bin/bash
echo -e "Content-Type: text/html\n"
env
响应:
HTTP/1.1 200 OK
Date: Sun, 23 Oct 2016 13:10:01 GMT
Server: Apache/2.4.18 (Ubuntu)
X-Author: https://phuker.github.io/
Vary: Accept-Encoding
Content-Length: 1180
Connection: close
Content-Type: text/html
SERVER_SIGNATURE=<address>Apache/2.4.18 (Ubuntu) Server at 192.168.245.136 Port 80</address>
HTTP_USER_AGENT=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.7 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.7
SERVER_PORT=80
HTTP_HOST=192.168.245.136
(省略)
Hello World¶
Linux/Windows,C 语言,请求的时候要请求编译好的二进制程序。
#include <stdio.h>
int main()
{
printf("Content-Type: text/html\n\n");
printf("Hello World!\n");
return 0;
}
效果:
一个 say hello 的动态网页¶
Linux/Windows,Python,头部需要加 Python 可执行文件的位置。对于 Windows,则可能是 #!C:/Python27/python.exe
。
#!/usr/local/bin/python
import os
import urllib
import sys
import cgi
print 'Content-Type: text/html\n'
postData = sys.stdin.read()
if postData != '':
items = postData.split('&')
for item in items:
key,value = urllib.splitvalue(item)
if key == 'name':
print '<h1>Hello, %s</h1>' % (cgi.escape(urllib.unquote_plus(value)),)
print '''<form action="" method="POST">
<input name="name" type="text" placeholder="Your name" />
<input type="submit" />
</form>'''
print os.environ['QUERY_STRING']
效果如下图。此程序读取 POST 数据(stdin),Tom 未在 URL 中出现。
另一个 say hello 的动态网页¶
Windows/Linux,C 语言。此处没有做 URL decode。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int i;
char * query;
query = getenv("QUERY_STRING");
printf("Content-type:text/html\n\n");
if(getenv("QUERY_STRING") != NULL && strlen(getenv("QUERY_STRING")) > 5)
{
printf("<h1>Hello %s</h1><br />\n", query + 5);
}
printf("<form action=\"\" method=\"GET\"><input type=\"text\" name=\"name\" autofocus/><input type=\"submit\"/></form>");
return 0;
}
执行任意命令的 Webshell,及其过程与经验¶
Linux,shell script
#!/bin/bash
echo -e 'Content-Type: text/html\n'
echo '<h1>I am using cgi (shell script)</h1>'
echo '<form action="" method="GET">'
echo '<input type="text" name="cmd" autofocus />'
echo '<input type="submit" />'
echo '</form>'
echo -e '\n<pre>\n'
#echo ${QUERY_STRING}
cmd=${QUERY_STRING#'cmd='}
#echo ${cmd}
cmd=${cmd:-'ping -c 2 baidu.com'}
cmd=$(echo ${cmd}| python -c 'import sys;import urllib;sys.stdout.write(urllib.unquote_plus(sys.stdin.read()))')
echo ${cmd}
echo '<hr />'
eval ${cmd} 2>&1
#ping -c 2 baidu.com
echo -e '\n</pre>\n'
效果:
由于洒家对 shell script 这门精妙的语言不甚了解,以为在 shell script 中执行一个命令(字符串)直接写上即可,一开始上文加粗的字体写的命令是 ${cmd} 2>&1
。这一个脚本在命令执行的时候有一些奇怪的地方。例如执行 python -c 'import this'
时,会报错:
File "", line 1
'import
^
SyntaxError: EOL while scanning string literal
这个错误输出和下面的情况的输出相同:
user@localhost:/usr/lib/cgi-bin$ cmd=python\ -c\ \'import\ this\'
user@localhost:/usr/lib/cgi-bin$ echo $cmd
python -c 'import this'
user@localhost:/usr/lib/cgi-bin$ $cmd
File "<string>", line 1
'import
^
SyntaxError: EOL while scanning string literal
user@localhost:/usr/lib/cgi-bin$ eval $cmd
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
经过一番研究之后洒家恍然大悟,这个报错表示,Python 收到的参数(sys.argv[2]
)为 'import
,单引号的作用不是命令行中参数的边界,而是直接作为参数的一部分传进了 Python。也就是说,直接执行 ${cmd}
相当于 Python 中的
subprocess.call(["python","-c","'import","this'"])
一个字符串变量直接执行会有问题,正确的做法要用 eval
命令“扫描两次”,才能正确解析。使用 eval ${cmd}
使 shell 重新扫描命令,把 import
和 this
看做同一个参数,相当于 subprocess.call(["python","-c","import this"])
。
总而言之,这次的错误类似于这种情况:
user@localhost:/usr/lib/cgi-bin$ pipe="|"
user@localhost:/usr/lib/cgi-bin$ ls $pipe grep sh
ls: cannot access '|': No such file or directory
ls: cannot access 'grep': No such file or directory
ls: cannot access 'sh': No such file or directory
user@localhost:/usr/lib/cgi-bin$ eval ls $pipe grep sh
env_var.sh
test.sh
使用 Windows batch script(批处理)写的一个功能有限的 webshell¶
批处理,这个的坑也有点多,而且洒家也不太熟悉,只能写这么多了。
@echo off
echo Content-Type: text/html; charset=GBK
echo.
echo ^<h1^>batch webshell^</h1^>
echo ^<form action="" method="GET"^>
echo ^<input type="text" name="cmd" autofocus /^>
echo ^<input type="submit" /^>
echo ^</form^>
echo ^<pre^>
set ccmmdd=%QUERY_STRING:~4%
set ccmmdd=%ccmmdd:+= %
set ccmmdd=%ccmmdd:^%20= %
echo ^<textarea style="width:100%%;height:60%%;"^>
%ccmmdd%
echo ^</textarea^>
echo ^</pre^>